NOTE: The "reactivity Transform" RFC has been concluded and the team has decided not to go ahead with this. The information in the following post are informative to learn what was being discussed, but will not be usable as this feature is not going to be added within the Vue framework.
In this article, we discuss how to omit .value
in VueJS refs, creating APIs by converting them into reactive variables using macros.
Introduction
When Vue 3 first came around, it introduced the Composition API. The API permitted greater code re-usability, and provided 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 skills
- A basic understanding of how to use refs and reactivity in Vue
The functionality discussed in this article is purely opt-in, and existing behavior 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 along 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 the 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, 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 accessprops
asprops.x
in 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
props
declaration, 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 clean up your codebase 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!