Reactivity in Vue 2, 3, and the Composition API
Anthony Gore | March 5th, 2017 (Updated May 18th, 2020) | 7 min read
One of the features of Vue that first hooked me and many other developers is its reactivity system.
It's not just the ease in which it allows you to build dynamic UIs, but the fact that it just works without you having to think about it or even understand it.
If you want to become a more advanced Vue developer, however, and especially if you want to use advanced features like the Composition API, it's important to have some level of understanding of how reactivity works under the hood.
In this article, I'll tell the story of why reactivity was created, how it works in Vue 2, and how it's evolved to support powerful Vue 3 features like the Composition API.
Table of contents:
What is reactivity in Vue?
On day #1 of using Vue, the feature that will probably stand out to you most is how effortless it is to get Vue to link a JavaScript data model to the rendered page.
When you then modify that data during the app lifecycle, like magic, the UI you've created from this data will be updated, too.
For example, say you've got a data property message
in a Vue component and you're rendering this message in the view with a text interpolation:
Vue.component("my-component", {
data: {
message: "Hello, world"
},
template: "<p>{{ message }}</p>"
});
This is what will be rendered when the app instantiates:
<p>Hello, world</p>
What will happen when you modify message
during the app lifecycle, for example, in a method?
methods: {
updateMessage () {
this.message = "Goodbye, world";
}
}
Vue will automatically update the view to reflect this:
<p>Goodbye, world</p>
Even though this usage of reactivity is one of the key feature of Vue.js, it is actually just one possible use.
Reactive data can be more broadly thought of as data that causes some intended side effect when accessed or modified.
The intended side effect may be an update of the DOM, the re-calculation of a computed property, or some custom functionality that the developer provides.
Why you need a deeper understanding of reactivity
If the success of Vue had to be attributed to just one thing, I'd wager it's the fact that you can use it to build a robust reactive UI without understanding a thing about how reactivity works.
However, if you want to become an advanced Vue developer, understanding more about reactivity will allow you to:
- Avoid the shortcomings of reactivity (especially in Vue 2)
- Squeeze additional performance out of Vue
- Use advanced features including the Composition API
To begin this understanding, we need to be aware of the JavaScript features that underpin reactivity.
Getters and setters
Did you know that you can alter the way a JavaScript object is written or read? This can be done by providing a custom get
or set
method for that object.
For example, if you wanted to automatically console log the value of an object any time its modified, you could do that by defining a custom set
method (aka "setter").
const obj = {
value: "Hello, world",
set message (newVal) {
this.value = newVal;
console.log(newVal);
}
get message () {
return this.value;
}
};
obj.message = "Goodbye, world";
// console: Goodbye, world
Instead of logging to the console, what if we used the setter to update the DOM? This is the mechanism that allows reactivity to work.
Vue 2 reactivity
In very oversimplified terms, Vue 2 makes data reactive by walking through data each property, computed property, component prop, etc that the user has declared and tacks on custom getters and setters that will trigger side effects when the data is modified.
Say you have a Vue component like this:
const data = {
id: 1,
name: "My Item",
price: 9.99
}
Vue.component("my-item", { data });
At runtime, the data object would be walked through and getters and setters responsible for reactivity would be automatically added.
You can see the result of this process from a screenshot of this component's data object at runtime:
With reactive getters and setters added, modifying the data will now cause a re-render side effect:
methods: {
onClick () {
data.price = 10.99; // triggers re-render of component
}
}
Vue 2 reactivity caveats
This system can be used to great effect, but it has a shortcoming - reactivity can only be automatically applied when the app instantiates.
This means that if you decide to add a new property to the data object during the app lifecycle, reactivity will not be automatically provided. The reason for this is that there was no feasible way to observe such a change in JavaScript (at least not five years ago when Vue 2 was being designed).
For example, if we added a new data property qty
to this data model after the component is instantiated, the new property would not be reactive and therefore modifying it would not cause reactive side effects to trigger.
const data = {
id: 1, // reactive
name: "My Item", // reactive
price: 9.99 // reactive
};
Vue.component("my-item", { data });
data.qty = 1 // will not be reactive
The following runtime screenshot of the data model shows that qty
has been added as a property of the data object but, unlike its fellow properties, has no getters/setters defined:
Unexpectedly non-reactive properties usually cause problems downstream that can be difficult to diagnose. Have you ever spent an hour spent trying to figure out why a dynamic CSS rule is not being applied the second time you click a button? Etc etc.
In fear of these pernicious problems, Vue.js developers usually stay clear of any code solution that involves adding or removing data properties, and Vue has been designed in such a way that you rarely need to.
For dealing with edge cases, Vue provides API methods like Vue.set
to add a reactive data property after instantiation. But the point is that this is not automatic and it relies on the developer remembering to use it.
Vue.set(data, "qty", 1); // reactive
Other Vue 2 reactivity caveats include data property deletion and changes to reactive arrays. The Vue docs cover these limitations so I'll leave the explanation of Vue 2 reactivity there.
Vue 3 reactivity
The first water-cooler fact about Vue 3 reactivity you should be aware of is that the system was re-written and improved to leverage a new JavaSript feature Proxy
.
Proxies not only provides a way for the reactivity caveats of Vue 2 to be overcome but also allows the reuse of logic across components via the Composition API.
So what are proxies? They're a special type of object that wrap other objects you want to observe and are made aware of any kind of operation on that object during runtime.
For example, let's again create a reactive data object that logs modifications to the console, only this time we'll use the Proxy
feature:
let data = { message: "Hello, world" }; // (1)
const proxy = new Proxy(data, { // (2)
set (target, property, value) { // (3)
target[property] = value;
console.log(target);
}
});
proxy.message = "Goodbye, world";
/*
Console:
{
message: "Goodbye, world"
}
*/
- Data object we want to make reactive
- Declare a new
Proxy
object to wrap that data - Declare a
set
function that intercepts anyset
operations applied to the target data. Note that this is where reactivity side effects can now be triggered.
Since proxies watch the whole object new properties can be added during the app lifecycle and will still be automatically reactive:
proxy.newprop = null;
proxy.newprop = "test"
/*
Console:
{
message: "Goodbye, world",
newprop: "test"
}
*/
Note: being a new JavaScript feature from the ES2015 spec,
Proxy
is not compatible with some older browsers.
Reactivity and the Composition API
The Composition API is an alternative way of defining Vue components introduced in Vue 3. It allows you to declare component features inside the new setup
function instead of creating them as options on the component definition.
Under the Composition API reactive data is created with the new reactive
API:
import { reactive } from "vue";
export default {
setup () {
const data = reactive({ // (1)
message: "Hello, world"
});
// Optional: do other stuff (2)
return {
data // (3)
}
}
}
- Declare a reactive data object
- Optionally do other stuff with it
- To make it available to the render context (the template) simply return it from the
setup
function
I'm not going to show you all the other features of the Composition API (the docs will suffice here). Instead, I want to focus on point 2 in the code example above - what "stuff" might we do with reactive data before it gets passed on to the render context?
By allowing us to create data objects that are not bound to the context object (this
), the Composition API allows us to utilize reactive Vue data in a much more flexible way.
For example, some developers are creating Vuex-like state management with pure Vue 3 by passing reactive data objects around the app (since reactive data now doesn't need to be bound to any one component).
Note: similar functionality was also provided to Vue 2 via the
Vue.observable()
API introduced in Vue 2.6, though it's not as flexible as the Composition API.
But the main intention of the reactive
API, and indeed, the key use case of the Composition API, is to allow reactive data to be shared between components for the purpose of logic reuse.
Code sharing and reuse in Vue
In Vue 2, any sharing of component code required mixins because it's imperative to setting up reactivity that any properties you intend to be reactive are available to Vue at the time of instantiation.
Given that mixins are an anti-pattern for component architecture (something I argue in another article How the Vue Composition API Replaces Vue Mixins) code sharing and reuse is a weak point of Vue 2.
But by using the Composition API, data objects created using reactive
are not bound to the component instance. This means they can be shared like any other JavaScript data and retain their reactivity.
For example, you can create a module myReusableFeature
which returns reactive data to any components that would like to consume it:
import { reactive } from "vue";
import { myReusableFeature } from "./myReusableFeature";
export default {
setup () {
const { reactiveDataFromReusableFeature } = myReusableFeature();
return {
reactiveDataFromReusableFeature
}
}
}
More possibilities with Vue 3 reactivity
With the decoupling of internal reactivity into public APIs like reactive
and ref
, (and some other advanced features - see Advanced Reactivity APIs), the Composition API allows much more fine-grained control over reactivity.
But, under the hood, reactivity is roughly the same concept as it was from Vue 2, and since the new APIs are optional and tree-shakable they won't get in your way if you want to keep using Vue 3 they way you've been using Vue 2.
About Anthony Gore
If you enjoyed this article, show your support by buying me a coffee. You might also enjoy taking one of my online courses!
Click to load comments...