Reactivity: Vue 2 vs Vue 3

More often that not, us developers are spoiled by magic functionality in the libraries and frameworks that we use in our everyday code. The reactivity system in Vue is one of these magical tools that just somehow works. But do you actually understand what is happening behind the scenes?

Vue, like its sibling frameworks, brings a unique characteristic to JavaScript that we didn’t have as a possibility before: reactivity. In this article, I’ll walk through how Vue 2 accomplishes its reactivity, then look at how Vue 3 will bring an entirely new form of reactivity.


Why we need reactivity

JavaScript, like many other languages, gets executed in a sequential order. What this means is that the user’s computer reads every line of code sequentially, from top to bottom. But why is this important? And what does it have to do with reactivity?

Consider the following example (this is vanilla JavaScript).

    let avocados = 5;
    let breadSlices = 10;
    let availableSammiches = 0;
    
    function countSammiches() {
      if (breadSlices / 2 < avocados) {
        availableSammiches = avocados;
      } else {
        availableSammiches = breadSlices / 2;
      }
    }
    
    countSammiches();
    
    // You can have: 5 sammiches
    console.log('You can have: ' + availableSammiches + ' sammiches');
    
    function nomSammich() {
      console.log("nomnomnom");
      avocados--;
      breadSlices = breadSlices - 2;
    }
    
    nomSammich();
    
    // You can have: 5 sammiches
    console.log(availableSammiches); // Output: 5

If we run the above code you’re going to get the following output to your console:

    You can have: 5 sammiches 
    nomnomnom 
    You can have: 5 sammiches

As simple as the above example is, I bet you can already see the problem. Since our code is not reactive, even though we happily nommed some of our breads and avocados, we are still getting 5 available sammiches as a result. Clearly, this is not the intended result.

I don’t know about you, but I don’t want to have to call countSammiches manually every time my fridge gets some movement.

By the way, if you want to try it yourself, here’s a Codesandbox you can look at: https://codesandbox.io/s/reactivity-vanilla-example-7zf3p

Frontend developers were faced with this type of scenario before frameworks like Vue came along and made our lives easier and substantially more fun.

Back in the day, we would have to implement a solution where the remaining bread count and the avocado count were kept in global variables, the calculation became a function, and you would have to manually re-trigger this function every time either your avocado or bread stash was diminished.

Let’s just say it got really messy, really fast - an innumerable amount of sammiches were lost and unaccounted for. It was chaos.

https://media.giphy.com/media/jMZ3hBtP8LMVa/giphy.gif

Between then and now, a lot has happened. JavaScript is definitely not in the state where it used to be 10 years ago. We don’t nom avocado sammiches anymore; we make toast. But most importantly, ECMAScript5 brought a method that changed everything.


Meet Object.defineProperty()

In order to better understand Vue 2 's reactivity system, and what Vue 3’s changes to this system are, we first have to understand Object.defineProperty.

When developers were trying to solve the reactivity dilemma for scripts such as the one we built earlier in the article, an idea came to mind: what if somehow we could know when a piece of code uses this property, then we could automatically call any functions that update related values. This is what you know as reactivity in today’s frameworks. You have a property named avocados in your component, and it magically updates everywhere that uses it.

Let’s start by first understanding what Object.defineProperty brought to the table. The short answer is: it gave us the ability to define a get and set function that allows us to intercept the default behavior of setting or getting an object’s property.

If you come from a programming background with an Object Oriented language, you may be already familiar with the concepts of getters and setters, but let’s look at an example of what this means.

Imagine you have a bank account. That account holds a numeric value of money that you currently have. Whenever you access this bank account and withdraw from it, you are basically subtracting an amount of money, and when you deposit, you are adding an amount.

Imagine now that this account was an object, with a bunch of properties like the name of the holder, and the actual money amount.

    const account = {
      name: 'Marina Mosti',
      money: 10 
    }

Now think about this very real, very serious feature. We want to make sure, through terrible UX, that we notify the user how many avocados they can currently purchase within a console.log, every time their money increases or decreases. We’ll say for example purposes that each avocado is worth $2—being a millennial is expensive.

You could say, “Oh Marina, let’s just make a function that calculates this avocado amount and return a value.”

But then I have to go all around my codebase looking for [account.money](http://account.money) uses and manually calling the new function. That sounds like a lot of work and not what reactivity is all about. Here’s where a setter becomes useful.

Object.defineProperty gives us the ability to redefine how an object is either accessed (the getter) or set (that’s the setter 🤯), so that we can pre-process it however we see fit.

Consider the following:

    const account = {
      name: "Marina Mosti",
      money: 10 // That's 10 euros, so 5 avocados - 2 euros per
    };
    
    let money = account.money; // default value is current value
    Object.defineProperty(account, "money", {
      get() {
        return money
      },
      set(val) {
        const avocadoCost = 2
        money = val; // Normal setter behavior, this stores the new value
        if (val <= 2) {
          console.log("You need at least 2 euros to buy avocados. PANIC.");
        } else {
          console.log("You can now afford: " + money / avocadoCost + ' avocados');
        }
      }
    });
    
    account.money = 0;
    // Output: You need at least 2 euros to buy avocados. PANIC.
    
    account.money = 40;
    // Output: You can now afford: 20 avocados

Notice how we’re defining our two functions, get() and set(val). Whenever the money property is accessed or read, we are going to execute the block inside of get, which is just the default behavior: return the current value of money.

However, whenever the value of money is set, we are going to first store it inside money, as default behavior would, but then we will evaluate if the 💰can buy some 🥑and notify the user accordingly.

Setting aside the silliness of the example, this starts to light some bulbs. With this API, we finally have a way to not only know when a property of an object is being accessed or modified, but also to execute some of our own code whenever it happened. And alas, the fundamentals of the Vue 2 reactivity system were born.

Now I know what you’re thinking,“Wait… but how?”

Thankfully Gregg has a comprehensive course in Advanced Vue Components that covers in-depth the topic of Vue 2’s reactivity system. I highly recommend going through it at least once, so you can demystify the magic behind Vue.


A gaze into the future, Vue 3 reactivity

Even as amazing as Vue 2’s reactivity system is, it came with a few caveats that were a bit annoying to deal with.

Have you ever had to this.$set an object’s property because adding it directly wouldn’t make it reactive? Ever broke your head trying to figure out why setting an array’s index directly would break the reactivity system completely if not done through set?

If you need a refresh on Vue.set and what it actually does, you can check out my article about it on dev.to.

As we saw in the last part of the article, in order for Vue to be able to react to your variables, Vue had to do some heavy lifting by overwriting the object’s getter or setter. This of course meant that if Vue was not aware of this property at instantiation of your component, it would not know to look for changes on the properties and make the object or array item reactive, this is where $set came into play.

Just like with ECMAScript5 we got Object.defineProperty, with ES6 we got two new cool toys to play with: Proxy and Reflect. These can help us build reactivity.

Let’s start with Proxy and figure out how it plays out into the getter and setter world. We will start by creating a new Proxy. You can think of a proxy as a food delivery app—you can either directly call the restaurant to order the pizza or order through the company’s website, or you can use a proxy and place your order through the app.

If you order directly through the pizza’s website, you get the default behavior. You order, you receive, you nom.

If you order from the app, or the proxy, you get extra behavior like being able to track the delivery person and to leave a rating for the restaurant. But you also get the default behavior - with that added functionality on top of it.

Still with me? Let’s look at some code examples, and build onto the same idea we used in the Vue 2 demo.

    const account = {
      name: "Marina Mosti",
      money: 10
    };
    
    const accountProxy = new Proxy(account, {});
    
    console.log(account.money); // Output: 10
    console.log(accountProxy.money); // Output: 10

Again, we have our base account object for our bank information. The feature is the same, we want to make it so that whenever the money property changes, we notify the user of their avocado-purchasing abilities.

The interesting part here comes when we create our new Proxy. Notice the two console.log statements in the end. They both output the same value even though one is calling the account's money property, and the other one is calling the property through the accountProxy.

If we think about the pizza example, we are getting the exact same experience through our delivery app proxy as we are if we go to the restaurant’s website.

Did you notice the {} as the second parameter in the new Proxy constructor? This is called a handler. In here, we can start having some getter/setter fun. Let’s make our proxy enhance the account experience.

    const account = {
      name: "Marina Mosti",
      money: 10
    };
    
    const avocadoCost = 2;
    
    const accountProxy = new Proxy(account, {
      get(target, key) {
        console.log("Proxy getter was called for " + key);
        return target[key];
      }
    });
    
    console.log(account.money); // Output: 10
    console.log(accountProxy.money); // Output: Proxy getter was called for money 10

Let’s get started with the get method, since we are not going to do anything crazy with it. Notice that similarly to Object.defineProperty we are defining a get method inside the handler, and it receives two params: target and key.

The target is the object that we are proxying. In this case target is the same as account, since that is the object that we’re passing in as the first param to the Proxy constructor. Keep in mind though, that this target can also be an array, a function, or even another proxy.

The key in this case will be the property of the object that we’re trying to get. In the case of an array it would be the index.

Next up we console.log some information to know when we are accessing the getter through the proxy, and we finally return target[key].

Take notice finally of our two console logs in the bottom, in the first one we output only the value of 10 as it is a vanilla JS object’s property that we’re logging. In the second one, however, we first go through the proxy and get additional behavior. Amazing!

In the beginning of this section I told you we would be using two new cool toys, Proxy is the first one as we already saw, now it’s time to use Reflect.

Reflect in itself is an object that provides us with some methods for interceptable JavaScript operations. The methods are in fact the exact same that you can use inside your Proxy handler. Long story short, Reflect in the context of Vue allows us to ensure that this doesn’t break when the object has values or functions it inherited from another object.

Let’s make a small modification to our proxy’s handler, and since we’re defining get, return the value with Reflect.get. Remember that Proxy handlers and Reflect share the same method names.

    [...]
    get(target, key, receiver) {
        console.log("Proxy getter was called for " + key);
        return Reflect.get(target, key, receiver)
      },
    [...]

Notice first that we added a third parameter to our get function, receiver. This new param is going to be either the proxy itself, or an object that inherits from it. The important thing to know here is that Reflect needs it as a third parameter, and we will pass it along for it to do its job.

Did you notice that the params for get in the handler and Reflect.get are the exact same ones? You can also write the return statement using ...arguments like so:

    return Reflect.get(...arguments)

Rejoice! Our proxy now can handle get requests and is this proofed.

Let’s move on now to the interesting part of our demo, the set method.

    const account = {
      name: "Marina Mosti",
      money: 10
    };
    
    const avocadoCost = 2;
    
    const accountProxy = new Proxy(account, {
      get(target, key, receiver) {
        console.log("Proxy getter was called for " + key);
        return Reflect.get(target, key, receiver)
      },
      set(target, key, val, receiver) {
        if (key === "money") {
          if (val <= 2) {
            console.log("You need at least 2 euros to buy avocados. PANIC.");
          } else {
            console.log("You can now afford: " + val / avocadoCost + " avocados");
          }
        }
    
        return Reflect.set(target, key, val, receiver);
      }
    });
    
    console.log(account.money); // Output: 10
    console.log(accountProxy.money); // Output: Proxy getter was called for money 10
    
    accountProxy.money = 20; // Output: You can now afford: 10 avocados
    console.log(account.money); // Output: 20
    console.log(accountProxy.money); // Output: Proxy getter was called for money 20
    
    account.money = 10; // NO OUTPUT, default behavior!
    console.log(account.money); // Output: 10
    console.log(accountProxy.money); // Output: Proxy getter was called for money 10

Notice that a set method has been added to the handler. The set method receives a total of four params.

The first one is the target, just as in the get method this refers to the object that is being attached to the proxy - in this particular case, the account.

A key, which in this case is the property which is being set, same as get once again.

A third parameter val, which holds the new value that we’re going to assign to the key, and finally the receiver that we need to pass down to our Reflect.set call.

This time around we’re going to perform a check, and if the key that is being set is the money, we’re going to provide our avocado-calculating functionality through our proxy to the user.

Finally, we return the result of calling Reflect.set. This function takes four params, the same ones that were received by the handler’s set function. Reflect.set will make sure that the new value is correctly assigned to the target, in this example it means that [account.money](http://account.money) will be updated with the val.

Now take a look at the additional console logs. Notice that when we assign the money property, the value of 20 through the proxy, our set function is executed with extra functionality.

However, when we set the value of money to 10 directly from the original object through account.money = 10 we get the default JavaScript experience, but we do NOT lose it.

If you want to check this out for yourself, here’s a sandbox with the code: https://codesandbox.io/s/vue-3-reactivity-kydvd


At this point you may be wondering, “Okay, this is all really cool but why is it better than the Object.defineProperty solution?”

Remember how in the Vue 2 part of this article I was talking about how we currently have some caveats that need to be resolved with Vue.set? Vue 2 needs to overwrite the functionality of each property in the object with defineProperty to inject the reactivity methods, so if you all of a sudden decide to add a property to the account method there is no way for Vue to know that!

The solution right now is to have a method like Vue.set, where you explicitly tell Vue, Hey, listen! I’ve added a new property to this object, or a new item to the array and you need to make it reactive.

With the Proxy solution however, notice that we never explicitly do anything for a particular property in the account object. We are wrapping the object in a Proxy that will execute the set and get functions for ANY property that we use! 💡That’s right, even ones that are added on the fly.

https://media.giphy.com/media/yidUzHnBk32Um9aMMw/giphy.gif


Wrapping up

Vue 3’s reactivity system is built upon these two (relatively simple) concepts, but it holds a lot more under the hood than a few getters and setters. I recommend you check out our Vue 3 Reactivity course here on Vue Mastery to check out the nitty gritty details of the magic behind the framework.

If anything, now whenever someone says that they don’t understand how Vue works its magic, you can whip out some 🥑, clear your throat and say:

https://media.giphy.com/media/l4pTd67hgyKKnf9O8/giphy.gif

If you like this blog post, you may want to watch: