This is the fourth post in my 'Diving Into Vue 3' series. Today I hope to give a clear and practical description of how to use the new Reactivity APIs, focusing on ref
and reactive
. I'll also introduce how to use related helpers such as toRef
, toRefs
, and isRef
.
The other posts in this series that have already come out are:
- Diving Into Vue 3 - Getting Started
- Diving Into Vue 3 - The Setup Function
- Diving Into Vue 3: Methods, Watch, and Computed
But first, a little background on reactivity. Feel free to skip ahead to the section on how to make data properties reactive if you just want to learn about ref
and reactive
.
What is reactivity?
The term reactivity in relation to Vue generally refers to a feature where what you see on the screen automatically updates in-sync with any changes to the state. It's the Vue 'magic' that makes the template re-render instantly if a data property changes.
When talking about reactivity in JavaScript or in programming in general, the term means programming something to work the way Vue does by implementing a design pattern called the Observer Pattern, which is explained in Design Patterns for Humans as :
whenever an object changes its state, all its dependents are notified.
Vue automatically updating the DOM when a data property changes is a result of Vue being built using the Observer Pattern - Vue state is an object with properties that have dependencies, so if one of those properties changes, its dependents react to the change by updating if they need to, and that triggers the re-render in the browser.
JavaScript on its own is not reactive, as shown in this example:
let numWorkers = 50
let numManagers = 4
let totalEmployees = numWorkers + numManagers
console.log(totalEmployees) // 54
numWorkers = 48
console.log(totalEmployees) // Still 54
Vue is reactive because the core Vue.js team built it to be. So in the following example, totalEmployees
will automatically update anytime numWorkers
or numManagers
(two properties in the state object) changes:
data() {
//returns the state object
return { numWorkers: 4, numManagers: 6 }
},
computed: {
totalEmployees() {
// returns whatever the total is based on current state for numWorkers and numManagers
return this.numWorkers + this.numManagers
}
}
Reactivity in Vue 2
The reactivity system in both Vue 2 and Vue 3 is based on state being an object, but there are big differences in how the properties are made reactive.
In Vue 2 the data option returns an object:
data() {
return {
numWorkers: 4,
numManagers: 6
}
}
Under the hood, Vue 2 uses Object.defineProperty
to define all the data properties on a component instance, converting them to getters and setters. There's a deep dive into the Vue 2's reactivity system in the Vue.js docs that's worth spending some time with.
Because defining the properties happens at the time of the component instance's initialization, it results in some small drawbacks:
data properties cannot be added or deleted after component instance initialization. They have to be present during initialization for them to be reactive
If the data property is an array, it is not possible to set an item directly to the array through assignment by using the array index (as in
arr[0] = value
), and it also isn't possible to update the length of the array (as inarr.length = 0
)
This isn't a major problem because the Vue.$set
method can be used in cases where these updates need to be made after component instance initialization. However, Vue 3's reactivity system is so improved that now these issues are no longer a problem, making it unnecessary to use Vue.$set
.
Reactivity in Vue 3
Vue 3's reactivity system had a major rewrite from what it was in Vue 2. The fundamental idea of tracking all the data properties and their dependencies so they can update automatically is still the same, but Vue 3 now uses the JavaScript Proxy API to achieve this (instead of Object.defineProperty
like in Vue 2).
There's a rabbit hole to go down for anyone who wants to learn more about the Vue 3 reactivity system, and the Vue.js docs are fantastic. Even though I'm not going to explain it all (there's no way I could!), there are a few things I think are helpful to understand.
The docs state:
Proxy is an object that encases another object and allows you to intercept any interactions with that object.
Awareness of reactive properties using proxies is helpful when debugging code in the console. If I console.log
a reactive property such as this property company
:
const company = reactive({
employees: ['Tom', 'Sara', 'Joe'],
managers: ['Julie', 'Jorge'],
})
in the console, I see:
Clicking on it will open the object to show that there is a Handler and a Target. A proxy always contains a Handler and a Target, and since Vue 3 uses proxies, I find it helpful to be comfortable with the shape of this data as a proxy.
The Target is where to look for the actual values. It contains the data that I might be looking for. The Handler contains the special logic for making the data properties reactive. The Handler contains methods like get
and set
.
The Handler is the rabbit hole if you want to learn about reactivity. The Target is where I need to look for my data values.
Because reactive data is wrapped in a proxy, something to get used to when working with the data is the idea of having to 'unwrap' the data object to get to the value. After reading a lot of different resources about working with Vue 3 reactive data, I now feel comfortable with the idea that using strategies to 'unwrap' the data, such as destructuring or drilling down to the value property, are using the metaphor of unwrapping because Vue 3 reactive data is wrapped in a Proxy
object.
How to make data properties reactive
As I said earlier, if I want to make data properties reactive in Vue 2, I have to return them in an object inside the data option of the Options API.
data() {
return {
president: "Mickey Mouse",
vicePresident: "Donald Duck"
}
}
If I am using the Vue 3 setup function (see my post on the setup function if you need an explainer on that), I can make data reactive by using the reactive
or ref
helpers.
ref
For this first example, I will use ref
. I'm using ref
because "Mickey Mouse"
and "Donald Duck"
are strings, and the recommendation is to use ref
with primitive values (i.e. Javascript types that are not objects, such as strings, numbers, etc.)
First, I import ref
:
<script>import { ref } from "vue";</script>
Then in the setup
function, I set my variable to the ref()
helper, which takes in the initial value. I must include the data in the return object if I want it to be available to the template.
setup() {
let president = ref("Mickey Mouse");
let vicePresident = ref("Donald Duck");
return { president, vicePresident };
},
An important difference between ref
and reactive
is that if I want to do something to the value of my ref
properties inside the setup
function, I have to unwrap the object to access that value. So if I want to change the value of president
, I will change president.value
:
function changePresident() {
president.value = 'Goofy'
}
I don't have to worry about unwrapping the values for president
and vicePresident
in the template
. Vue can shallow unwrap those for me. 'Shallow unwrap' means the first level of properties in an object are available in the template without having to use .value
(but nested properties would still need to be unwrapped).
<template>
<div>
<p><b>President:</b> {{ president }}</p>
<p><b>Vice President:</b> {{ vicePresident }}</p>
</div>
</template>
FYI, it's fine not to use ref
if I don't need the data to be reactive, just writing the data like this:
setup() {
let president = "Mickey Mouse"
let vicePresident = "Donald Duck"
return { president, vicePresident };
},
But it would mean the data isn't reactive, so I can't ever see updates to the data. If I use a method to change the data, I would never see that update change anything on the screen, and I would have to be happy with Mickey Mouse and Donald Duck showing as president and vice president forever.
There are times when you don't need the data to be reactive, so in those instances, just don't use ref
or reactive
!
reactive
I can use reactive
for the same example, but I would only do so if I wanted the data to start out in the form of an object rather than separate string values. So in Vue 2, if I have this:
data() {
return {
executiveTeam: {
president: "Mickey Mouse",
vicePresident: "Donald Duck",
},
};
},
To change this to Vue 3 using reactive
, I will first import reactive
:
import { reactive } from 'vue'
In the setup
function, I will create an object for executiveTeam
and define the properties on the object. I can set the object to const
since the object itself won't change, just the properties inside.
setup() {
const executiveTeam = reactive({
president: "Mickey Mouse",
vicePresident: "Donald Duck",
});
return { executiveTeam };
},
And if I want to update the data, I do not have to unwrap it with .value
like I do with ref
.
function changePresident() {
executiveTeam.president = 'Goofy'
}
This is because reactive
is used with objects, and objects pass values by reference (which lends itself better to reactivity). Reactive references (ref
) are used for primitive types, and primitives in Javascript pass values by value, so Vue has to wrap them in an object to make them reactive. Since ref
properties are wrapped to make them reactive, they have to be unwrapped down to the .value
to get the value. Read more about this concept in the Composition API RFC if this concept is something you want to understand more deeply.
However, because I'm returning the object executiveTeam
and I want to access the properties president
and vicePresident
on that object in the template, I will have to drill down into the executiveTeam
object to get each property I need:
<template>
<div>
<p><b>President:</b> {{ executiveTeam.president }}</p>
<p><b>Vice President:</b> {{ executiveTeam.vicePresident }}</p>
</div>
</template>
I cannot destructure the object that I return because if I do, the properties inside executiveTeam
will lose reactivity. I'll demonstrate this in the next example to make this more clear.
When using reactive
to give an object's properties reactivity, like this:
const executiveTeam = reactive({
president: 'Mickey Mouse',
vicePresident: 'Donald Duck',
})
I cannot destructure to try and return those properties by their key, as in:
//LOSES REACTIVITY:
let { president, vicePresident } = executiveTeam
return { president, vicePresident }
This is where toRefs
comes in handy.
toRefs
The helper toRefs
will allow me to turn each of the properties in the object into a ref
, which means I won't have to use executiveTeam.president
in the template; I'll be able to just write president
. Here's the full example now using toRefs
:
<script>
import { reactive, toRefs } from "vue";
export default {
setup() {
const executiveTeam = reactive({
president: "Mickey Mouse",
vicePresident: "Donald Duck",
});
//toRefs allows me to destructure
let { president, vicePresident } = toRefs(executiveTeam);
return { president, vicePresident };
},
};
</script>
Since toRefs
turns each property into a ref
, I need to go back to unwrapping them down to their value using .value
if I want to do something to them in the setup
function:
function changePresident() {
president.value = 'Goofy'
}
toRef
Just like toRefs
, the helper toRef
is used to turn reactive object properties into reactive references (ref
), but I would use toRef
if I just need to turn one property in a reactive object into a ref
:
setup() {
const executiveTeam = reactive({
president: "Mickey Mouse",
vicePresident: "Donald Duck",
});
//toRef used to turn just one property into a ref
let presidentRef = toRef(executiveTeam, "president");
const changePresident = () => {
presidentRef.value = "Goofy";
};
return { presidentRef, changePresident };
},
I will have to use .value
if I want to update the ref's value inside the setup function, but in the template, Vue will unwrap president
for me:
<template>
<div>
<h1>Company Roles</h1>
<p><b>President:</b> {{ presidentRef }}</p>
<button @click="changePresident">Change President</button>
</div>
</template>
It can be challenging to remember which variables are reactive
properties and which ones are ref
. Something that helps is to use a naming convention where I add the suffix Ref to anything that is a ref
, such as presidentRef
. I don't have a lot of experience with Vue 3 yet, but for the time being, I plan to use that naming convention to see if it helps me get a better handle on the distinction between ref
and reactive
properties.
isRef
Vue 3 also provides the helper isRef
that I can use to check if something is a ref
.
console.log(isRef(executiveTeam.president)) //false
console.log(isRef(presidentRef)) //true
My Thoughts on the Vue 3 Reactivity API
This topic of ref
and reactive
has been the most challenging for me in my goal to learn Vue 3. There is more nuance to how these helpers are used in practice, and it becomes too much for an introduction post to try to cover all the different situations where I might have to make informed decisions about using ref
and/or reactive
and all the other helpers.
The Vue.js team is aware that this is one of the challenges of Vue 3 - the question of when to use ref
or reactive
does not always receive a simple answer. In the Composition API RFC they state:
Understandably, users may get confused regarding which to use between ref and reactive. First thing to know is that you will need to understand both to efficiently make use of the Composition API. Using one exclusively will most likely lead to esoteric workarounds or reinvented wheels.
I have come across many resources that suggest using just ref
or just reactive
to start. But I think it is worth the effort to learn the nuances of both. I agree with the Vue.js team: it's better to understand both ref
and reactive
if I'm going to use Vue 3 to its fullest potential. And that's what I plan to do.
While using just ref
for primitives and reactive
for objects is one suggested approach (suggested by the Vue.js team here), I would encourage you to dig deeper into the docs and resources out there to learn more about the Reactivity APIs. For a nuanced feature such as this, it's important to understand why certain approaches can be taken.
Conclusion
Please join me for my next post about reusability in Vue 3, including a discussion of composition functions (i.e. Vue composables). In my opinion, composables are the best thing about this new Composition API and they make it worth putting in the time to learn the harder concepts of Vue 3.
Please reach out on Twitter and let me know if you are enjoying this series on Vue 3.
Top comments (0)