How reactivity works in Vue 2 and 3

How reactivity works in Vue 2 and 3

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) and quantity
  • When clicking on the button, a function addQuantity is called to increase the variable quantity
  • 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.