JavaScript tools come with reactivity engines. By knowing what reactivity means and how it works inside, you can understand when your code works and fails. While also being able to identify bugs and prevent them from happening in the first place.
Let's start by looking at the word reactive. What does it mean?
Years ago, developers implemented pure HTML with JavaScript to achieve interactions on the page. The reactivity in the frontend was "archaic", but enough for that time.
Due to the increased complexity in the pages and apps, this solution was difficult to maintain. That's when libraries and frameworks came in, like Vue.
Instead of directly programming the DOM interaction, these tools handle that job, while proving a much more clean way to structure your project.
Defining reactive in the frontend community it's always going to be attached to frameworks, like React and Vue. What I want to share here it's not frameworks in frontend. And instead, how things work with Vue.
Here is how a code in Vue looks like:
<template>
<div>
<div>Price is ${{ price }}</div>
<div>Quantity is {{ quantity }}</div>
<div>Total is ${{ price * quantity}}</div>
<button @click="addQuantity">Add quantity</button>
</div>
</template>
<script>
export default {
data() {
return {
price: 10,
quantity: 0,
};
},
methods: {
addQuantity() {
this.quantity = this.quantity + 1;
},
},
};
</script>
What's happening is that:
- We have two variables, the
price
(always equal to 10) andquantity
- When clicking on the button, a function
addQuantity
is called to increase the variablequantity
- The total is updated on the page with
price * quantity
The
quantity
is more than just a variable, it's a state. States are how a reactive system can perform rendering in the page because is notified when a state changes.Let's take a closer look to the function
addQuantity
:addQuantity() {
this.quantity = this.quantity + 1;
}
When I started with React, it was clear to me that setState function was responsible for notifying React engine to update a state in the app (the function name already gives a good hint).
But in Vue, it's an assignment. And if it's procedural JavaScript being compiled (which it is), then there is got to be something hidden that makes the code reactive (and again, there is).
So how does Vue know when the
quantity
state has changed?Let's start with Vue 2.
Reactivity in Vue 2
The reactivity of states in Vue 2 is based on Object.defineProperty, a JavaScript implementation that can create an Observer Design Pattern. Have a look at this an example:
const data = { quantity: 1 };
Object.defineProperty(data, 'quantity', {
get () {
console.log('get of quantity was called');
},
set () {
console.log('set of quantity was called');
}
})
data.quantity; // this will log 'get of quantity was called'
data.quantity = 2; // this will log 'set of quantity was called'
This JavaScript implementation creates getter and setter functions. But if we want to change the
data
state, we need to create a copy of it (otherwise accessing or assigning a value with these functions would trigger an infinite loop).Using the same example, the
dataQuantityCopy
stores the value:const data = { quantity: 1 };
let dataQuantityCopy = data.quantity;
Object.defineProperty(data, 'quantity', {
get () {
return dataQuantityCopy;
},
set (value) {
dataQuantityCopy = value
}
})
data.quantity;
data.quantity = 2;
Now we have something close to a state, we can call other functions, or any reactivity required to update something when using the
get
and set
of Object.defineProperty
.A more advanced implementation could be achieved with
Object.keys(data).forEach
, so not only quantity
would work, but any data.This is a simplification of what's inside the core of Vue. When using Vue, developers don't need to worry about how to implement state management in JavaScript.
Limitations in Vue 2 (pitfalls to be aware)
This solution works well for primitive values, but what if the property is also an object or an array? What if adding new values that did not exist before in the
data
?In the Vue framework, there are caveats and workarounds for object and array type.
Objects:
Vue cannot detect property addition or deletion. The getters and setters are defined only on initialization of the component.
A workaround on this is to use Vue.set, which lets Vue knows there are changes in the object structure, and modify the getters and setters of the property.
Arrays:
Vue cannot detect changes by index (example:
item[index] = value
). The setter is not triggered.A workaround on this is to use either Vue.set or Array.splice. Both will let Vue knows there are changes.
Enter Vue 3
One of the main changes in the Vue core related to states is that
Object.defineProperty
was replaced by Proxy and Reflect.Using the same example as before, the code is still similar:
const data = { quantity: 1 }
const proxiedData = new Proxy(data, {
get(target, key) {
console.log('get of ' + key + ' was called');
return Reflect.get(target, key)
},
set(target, key, value) {
console.log('set of ' + key + ' was called');
return Reflect.set(target, key, value)
}
})
proxiedData.quantity; // this will log 'get of quantity was called'
proxiedData.quantity = 2; // this will log 'set of quantity was called'
The
Proxy
API works like the Object.defineProperty
. But the difference is that the getter and setter receive the property key being modified, which makes things easier to initialize the whole state to be reactivity (so new property additions are triggered, eliminating the caveats on Vue 2).The
Reflect
API works similarly to access or assignment, but since those are functions, they know how to get and set the data without triggering the Proxy again, thus avoiding infinite loops.Limitations in Vue 3
These APIs are available in ES6 and polyfill libraries like Babel don't support transpilations of
Proxy
.This means that those features don't work on old browsers, so Vue 3 is not compatible with IE11 (see more information in this RFC).
Conclusion
Vue 3 comes with significant improvements on reactivity in its core, without affecting the API for developers. We can continue to use it as before and without needing to worry about the previous caveats when dealing with objects and arrays.
This change came at a cost of dropping the support for IE11. So when picking libraries and frameworks for building an application, we should consider if supporting old browsers like IE11 is relevant, which might make Vue 3 not the best option.