How to omit .value in refs (Vue 3 Composition API)
A technical article that expounds on how we can omit using .value in VueJS ref creating APIs by converting them into reactive variables using macros.
Introduction
When Vue 3 first came around, it introduced the Composition API. The API allowed for greater code re-usability as well as a better way to organize Vue JS code. Along with the composition API, came the concept of refs. To access the value of a ref, you needed to append .value to the name of the ref variable. To tackle this, the Vue team came up with a solution (Reactivity Transform) that would allow us to create reactive variables without creating refs.
Prerequisites
This article is primarily aimed at intermediate Vue 3 developers and assumes three things.
- Intermediate knowledge of JavaScript
- Basic Vue 3 usage
- A basic understanding of how to use refs and reactivity in vue
The functionality discussed in this article is purely opt-in and existing behaviour is unnafected.
Tools needed
For this to work, you will need to be using vue@^3.2.25 and above. No additional dependencies are required. Vue 3.2.25+ ships an implementation under the package @vue/reactivity-transform. It is also integrated (with its APIs re-exported) in @vue/compiler-sfc so most userland projects won't need to explicitly install it.
Reactivity in Vue 3
Reactivity refers to the ability to keep track of changes that occur in our applications. One such way to achieve reactivity in Vue 3 is by using refs.
Creating Refs
The syntax for creating a ref would be something on the lines of this.
import { ref } from "vue";
// By wrapping our default value (true) with a ref, we tell vue to keep track of changes made to it
const isReading = ref(true);
This means that when the value of isReading changes, Vue knows about it and it can keep track of the changes. This means that UI is automatically updated whenever the value of isReading changes. In your template file, you would access the reactive value the same way you would access any variable, for example:
<template>
  <h1>{{ isReading ? "Shhh, I'm reading" : "Talk to me" }}</h1>
</template>
Using refs
That's all fine and dandy, but when you want to access or modify the value of the refs in the script, then you need to append a .value at the end of it. This is because ref() wraps the actual variable(isReading) in an object that can keep track of any changes made to it.
import { ref } from "vue";
const isReading = ref(true);
// prints an object that represents the ref object that wraps isReading
console.log(isReading);
// This is how you would need to access the value of isReading
console.log(isReading.value); // prints true
Reactivity Transform
  
  
  Removing the need for .value
The new Vue 3 syntax allows you to use refs without needing to use .value. To make this work, the Vue team implemented Reactivity Transform. This allows us to create reactive variables for every API that creates refs instead of using refs. This means we can use our variables without appending .value everywhere. Reactive variables do not need .value to be accessed while refs need you to append .value.
Previously we used to write code like this
const isReading = ref(true);
console.log(isReading.value);
which can now be written like this
// Prepending $ to ref makes $ref() a macro that wraps around the original ref()
const isReading = $ref(true);
console.log(isReading); // no need to write
Behind the scenes, Vue will unwrap the $ref() and compile into the original .value syntax we are used to writing. The only difference is that this time you don't have to write isReading.value everywhere. This is particularly useful in areas where the ref created is used in multiple places within a script.
It is also worth noting that every reactivity API that returns refs will have a $-prefixed macro equivalent.
These APIs include:
ref -> $ref
computed -> $computed
shallowRef -> $shallowRef
customRef -> $customRef
toRef -> $toRef
  
  
  Do you need to import $ref ?
Since $ref and equivalents are macros, they do not need to be imported. However, if you would like to import them explicitly, you can do so from vue/macros.
import { $ref } from "vue/macros";
  
  
  Convert an existing ref as reactive variable using $()
In situations where we have a function that returns a ref, the Vue compiler would not be able to know that the function will return a ref ahead of time. In such cases, we can wrap the function call with $() to explicitly convert it into a reactive variable.
function getIsReadingRef() {
  return ref(true);
}
const isReading = $(getIsReadingRef());
Destructuring objects of refs
Previously, if you tried to destructure an object that was a ref, the destructured variables would lose their reactivity.
Let's go with an example ref.
const getDefaultReader = () => ref({ name: "VueJS lover", timeOnPage: 30 });
// Vue will be able to tell when any part of `reader` changes
const reader = ref(getDefaultReader());
// Vue won't be able to tell when the values of `name` and `timeOnpage` change
const { name, timeOnPage } = ref(getDefaultReader());
With Reactivity transform, you can destructure the objects of refs and maintain reactivity. You do so by wrapping the value with a $().
// Vue will now be able to tell when the values of `name` and `timeOnpage` change
const { name, timeOnPage } = $(getDefaultReader());
The above code will compile to:
const __temp = getDefaultReader(),
  name = toRef(__temp, "name");
timeOnPage = toRef(__temp, "timeOnPage");
Reactive props destructuring
This example is from the original Reactivity Transform RFC.
There are two pain points with the current
defineProps()usage in<script setup>
- Similar to .value, you need to always accesspropsasprops.xin order to retain reactivity. This means you cannot destructure defineProps because the resulting destructured variables are not reactive and will not update.
- When using the type-only propsdeclaration, there is no easy way to declare default values for theprops. We introduced thewithDefaults()API for this exact purpose, but it's still clunky to use.
<script setup lang="ts">
interface Props {
  msg: string;
  count?: number;
  foo?: string;
}
const {
  msg,
  // default value just works
  count = 1,
  // local aliasing also just works
  // here we are aliasing `props.foo` to `bar`
  foo: bar,
} = defineProps<Props>();
watchEffect(() => {
  // will log whenever the props change
  console.log(msg, count, bar);
});
</script>
The above will be combined to the following in runtime
export default {
  props: {
    msg: { type: String, required: true },
    count: { type: Number, default: 1 },
    foo: String,
  },
  setup(props) {
    watchEffect(() => {
      console.log(props.msg, props.count, props.foo);
    });
  },
};
Using $$() to retain reactivity
To get around reactivity loss in certain scenarios, the $$() macro can be used.
Retaining reactivity when passing refs as function arguments
Consider a situation where you have a function that needs to accept a reactive variable as an argument.
function trackChange(isReading: Ref<boolean>) {
  watch(isReading, (isReading) => {
    console.log("isReading changed!", isReading);
  });
}
let isReading = $ref(true);
// This will not work
trackChange(isReading);
In such a case, reactivity is lost.The reason for this is that the isReading ref is actually unwrapped into isReading.value when being passed in as the argument for trackChange while trackChange expects an actual ref. The above code compiles to this:
import { ref } from "vue";
let isReading = ref(true);
// This is what is actually happening
trackChange(isReading.value);
To get around this, we can wrap the ref in $$() which tells the compiler not to append a .value to it.
// This will work
trackChange($$(isReading));
The above example compiles to this:
import { ref } from "vue";
let isReading = ref(true);
// This is what we want - the isReading variable should be passed as a ref
trackChange(isReading);
Retaining reactivity when returning inside function scope
Another scenario where reactivity is lost is when we are returning reactive variables from within a function.
function useMouse() {
  let x = $ref(0);
  let y = $ref(0);
  // listen to mousemove...
  // doesn't work!
  return {
    x,
    y,
  };
}
Similar to the example with passing refs as arguments, the above return statement compiles to:
return {
  x: x.value,
  y: y.value,
};
In order to maintain the reactivity of x and y, we can wrap the entire return statement with the $$() macro.
function useMouse() {
  let x = $ref(0);
  let y = $ref(0);
  // listen to mousemove...
  // This works
  return $$({
    x,
    y,
  });
}
Retaining reactivity on destructured props
$$() works on destructured props since they are reactive variables as well. The compiler will convert it with toRef for efficiency:
const { count } = defineProps<{ count: number }>();
passAsRef($$(count));
compiles to:
setup(props) {
  const __props_count = toRef(props, 'count')
  passAsRef(__props_count)
}
TypeScript & Tooling Integration
Vue will provide typings for these macros (available globally) and all types will work as expected. There are no incompatibilities with standard TypeScript semantics so the syntax would work with all existing tooling.
This also means the macros can work in any files where valid JS/TS are allowed - not just inside Vue SFCs.
Since the macros are available globally, their types need to be explicitly referenced (e.g. in a env.d.ts file):
/// <reference types="vue/macros-global" />
When explicitly importing the macros from vue/macros, the type will work without declaring the globals.
Conclusion
By taking advantage of the macros added to Vue 3, you can drastically cleanup your code base by getting rid of .value usage. You also get to preserve reactivity within your application when destructuring reactive variables as well as props when using the Composition API and defineProps().
If you'd like to read more on the same, you can do so in the official Vue JS RFC discussion for the feature.
I do hope you find this helpful in reducing your code footprint and making your general life easier. The next time you think of using .value for your refs, remember that you don't have to. With that, thanks for stopping by(e)!
This Dot Labs is a development consultancy focused on providing staff augmentation, architectural guidance, and consulting to companies.
We help implement and teach modern web best practices with technologies such as React, Angular, Vue, Web Components, GraphQL, Node, and more.
 

 
                       
    
Top comments (3)
Would be worth to say that this feature is still experimental and share links to Vue documentation: vuejs.org/guide/extras/reactivity-...
And @mreduar I hope you noticed while changing the value you still use the
isReading.value = falsesyntaxHow cool, I didn't know this. Is there anything against using
$ref()instead ofref()?Edit:
I found a problem and it is that the editor says that there is an error because it is trying to modify a constant.