Reactivity systems are one of the key parts of modern front-end frameworks. They’re the magic wand which makes apps highly interactive, dynamic, and responsive. Understanding what a reactivity system is and how it can be applied in practice is a crucial skill for every web developer.
A reactivity system is a mechanism which automatically keeps in sync a data source (model) with a data representation (view) layer. Every time the model changes, the view is re-rendered to reflect the changes.
Let’s take a simple Markdown editor as an example. It usually has two panes: one for writing the Markdown code (which modifies the underlying model), and one for previewing the compiled HTML (which shows the updated view). When you write something in the writing pane, it’s immediately and automatically previewed in the previewing pane. Of course, this is just a simple example. Often things are far more complex.
In many cases, the data we want to display depends on some other data. In such a scenario, the dependencies are tracked and the data is updated accordingly. For example, let’s say we have a fullName
property, which depends on firstName
and lastName
properties. When any of its dependencies are modified, the fullName
property is automatically re-evaluated and the result is displayed in the view.
Now that we’ve established what reactivity is, it’s time to learn how the new Vue 3 reactivity works, and how we can use it in practice. But before we do this, we’ll take a quick look at the old Vue 2 reactivity and its caveats.
A Brief Exploration of Vue 2 Reactivity
Table of Contents
Reactivity in Vue 2 is more or less “hidden”. Whatever we put in the data
object, Vue makes it reactive implicitly. On the one hand, this makes the developer’s job easier, but on the other hand it leads to less flexibility.
Behind the scenes, Vue 2 uses the ES5 Object.defineProperty() to convert all of the data
object’s properties into getters and setters. For each component instance, Vue creates a dependencies watcher instance. Any properties collected/tracked as dependencies during the component’s render are recorded by the watcher. Later on, when a dependency’s setter is triggered, the watcher is notified and the component re-renders and updates the view. This is basically how all the magic works. Unfortunately, there are some caveats.
Change Detection Caveats
Because of the limitations of Object.defineProperty()
, there are some data changes that Vue can’t detect. These include:
- adding/removing a property to/from an object (such as
obj.newKey = value
) - setting array items by index (such as
arr[index] = newValue
) - modifying the length of an array (such as
arr.length = newLength
)
Fortunately, to deal with these limitations Vue provides us with the Vue.set API method, which adds a property to a reactive object, ensuring the new property is also reactive and thus triggers view updates.
Let’s explore the above cases in the following example:
<div id="app"> <h1>Hello! My name is {{ person.name }}. I'm {{ person.age }} years old.</h1> <button @click="addAgeProperty">Add "age" property</button> <p>Here are my favorite activities:</p> <ul> <li v-for="item, index in activities" :key="index"> {{ item }} <button @click="editActivity(index)">Edit</button> </li> </ul> <button @click="clearActivities">Clear the activities list</button> </div>
const App = new Vue({ el: '#app', data: { person: { name: "David" }, activities: [ "Reading books", "Listening music", "Watching TV" ] }, methods: { // 1. Add a new property to an object addAgeProperty() { this.person.age = 30 }, // 2. Setting an array item by index editActivity(index) { const newValue = prompt('Input a new value') if (newValue) { this.activities[index] = newValue } }, // 3. Modifying the length of the array clearActivities() { this.activities.length = 0 } } });
Here’s a CodePen example.
In the above example, we can see that none of the three methods is working. We can’t add a new property to the person
object. We can’t edit an item from the activities
array by using its index. And we can’t modify the length of the activities
array.
Of course, there are workarounds for these cases and we’ll explore them in the next example:
const App = new Vue({ el: '#app', data: { person: { name: "David" }, activities: [ "Reading books", "Listening music", "Watching TV" ] }, methods: { // 1. Adding a new property to the object addAgeProperty() { Vue.set(this.person, 'age', 30) }, // 2. Setting an array item by index editActivity(index) { const newValue = prompt('Input a new value') if (newValue) { Vue.set(this.activities, index, newValue) } }, // 3. Modifying the length of the array clearActivities() { this.activities.splice(0) } } });
Here’s a CodePen example.
In this example, we use the Vue.set
API method to add the new age
property to the person
object and to select/modify a particular item from the activities array. In the last case, we just use the JavaScript built-in splice()
array method.
As we can see, this works, but it’s a bit hacky and leads to inconsistency in the codebase. Fortunately, in Vue 3 this has been resolved. Let’s see the magic in action, in the following example:
const App = { data() { return { person: { name: "David" }, activities: [ "Reading books", "Listening music", "Watching TV" ] } }, methods: { // 1. Adding a new property to the object addAgeProperty() { this.person.age = 30 }, // 2. Setting an array item by index editActivity(index) { const newValue = prompt('Input a new value') if (newValue) { this.activities[index] = newValue } }, // 3. Modifying the length of the array clearActivities() { this.activities.length = 0 } } } Vue.createApp(App).mount('#app')
Here’s a CodePen example.
In this example, which uses Vue 3, we revert to the built-in JavaScript functionality, used in the first example, and now all methods work like a charm.
In Vue 2.6, a Vue.observable() API method was introduced. It exposes, to some extent, the reactivity system allowing developers to make objects reactive explicitly. Actually, this is the exact same method Vue uses internally to wrap the data
object and is useful for creating a minimal, cross-component state store for simple scenarios. But despite its usefulness, this single method can’t match the power and flexibility of the full, feature-rich reactivity API which ships with Vue 3. And we’ll see why in the next sections.
Note: because Object.defineProperty()
is an ES5-only and un-shimmable feature, Vue 2 doesn’t support IE8 and below.
How Vue 3 Reactivity Works
The reactivity system in Vue 3 was completely rewritten in order to take advantage of the ES6 Proxy and Reflect APIs. The new version exposes a feature-rich reactivity API which makes the system far more flexible and powerful than before.
The Proxy API allows developers to intercept and modify low-level object operations on a target object. A proxy is a clone/wrapper of an object (called target) and offers special functions (called traps), which respond to specific operations and override the built-in behavior of JavaScript objects. If you still need to use the default behavior, you can use the corresponding Reflection API, whose methods, as the name suggests, reflect those of the Proxy API. Let’s explore an example to see how these APIs are used in Vue 3:
let person = { name: "David", age: 27 }; const handler = { get(target, property, receiver) { // track(target, property) console.log(property) // output: name return Reflect.get(target, property, receiver) }, set(target, property, value, receiver) { // trigger(target, property) console.log(`${property}: ${value}`) // output: "age: 30" and "hobby: Programming" return Reflect.set(target, property, value, receiver) } } let proxy = new Proxy(person, handler); console.log(person) // get (reading a property value) console.log(proxy.name) // output: David // set (writing to a property) proxy.age = 30; // set (creating a new property) proxy.hobby = "Programming"; console.log(person)
Here’s a CodePen example.
To create a new proxy, we use the new Proxy(target, handler)
constructor. It takes two arguments: the target object (person
object) and the handler object, which defines which operations will be intercepted (get
and set
operations). In the handler
object, we use the get
and set
traps to track when a property is read and when a property is modified/added. We set console statements to ensure that the methods work correctly.
The get
and set
traps take the following arguments:
target
: the target object which is wrapped by the proxyproperty
: the property namevalue
: the property value (this argument is used only for set operations)receiver
: the object on which the operation takes place (usually the proxy)
The Reflect API methods accepts the same arguments as their corresponding proxy methods. They’re used to implement the default behavior for the given operations, which for the get
trap is returning the property name and for the set
trap is returning true
if the property was set or false
if not.
The commented track()
and trigger()
functions are specific to Vue and are used to track when a property is read and when a property is modified/added. As a result, Vue re-runs the code that’s using that property.
In the last part of the example, we use a console statement to output the original person
object. Then we use another statement to read the property name
of the proxy
object. Next, we modify the age
property and create a new hobby
property. Finally, we output the person
object again to see that it has been updated correctly.
And this is how Vue 3 reactivity works in a nutshell. Of course, the real implementation is way more complex, but hopefully the example presented above is enough for you to grasp the main idea.
There’s also a couple of considerations when you use Vue 3 reactivity:
- it only works on browsers supporting ES6+
- the reactive proxy isn’t equal to the original object
Continue reading Understanding the New Reactivity System in Vue 3 on SitePoint.