Diving Into Vue 3 - The Reactivity API

Share

  1. Blog
  2. 2022
  3. 02
  4. Diving Into Vue 3 - The Reactivity API

This is the fourth post in my 'Diving Into Vue 3' series. Today I hope to give a clear and practical description of how to use the new Reactivity APIs, focusing on ref and reactive. I'll also introduce how to use related helpers such as toRef, toRefs, and isRef.

The other posts in this series that have already come out are:

But first, a little background on reactivity. Feel free to skip ahead to the section on how to make data properties reactive if you just want to learn about ref and reactive.

What is reactivity?

The term reactivity in relation to Vue generally refers to a feature where what you see on the screen automatically updates in-sync with any changes to the state. It's the Vue 'magic' that makes the template re-render instantly if a data property changes.

When talking about reactivity in JavaScript or in programming in general, the term means programming something to work the way Vue does by implementing a design pattern called the Observer Pattern, which is explained in Design Patterns for Humans as :

whenever an object changes its state, all its dependents are notified.

Vue automatically updating the DOM when a data property changes is a result of Vue being built using the Observer Pattern - Vue state is an object with properties that have dependencies, so if one of those properties changes, its dependents react to the change by updating if they need to, and that triggers the re-render in the browser.

JavaScript on its own is not reactive, as shown in this example:

let numWorkers = 50
let numManagers = 4
let totalEmployees = numWorkers + numManagers

console.log(totalEmployees) // 54

numWorkers = 48

console.log(totalEmployees) // Still 54

Vue is reactive because the core Vue.js team built it to be. So in the following example, totalEmployees will automatically update anytime numWorkers or numManagers (two properties in the state object) changes:

  data() {
    //returns the state object
    return { numWorkers: 4, numManagers: 6 }
  },
  computed: {
    totalEmployees() {
      // returns whatever the total is based on current state for numWorkers and numManagers
      return this.numWorkers +  this.numManagers
    }
  }

Reactivity in Vue 2

The reactivity system in both Vue 2 and Vue 3 is based on state being an object, but there are big differences in how the properties are made reactive.

In Vue 2 the data option returns an object:

  data() {
    return {
      numWorkers: 4,
      numManagers: 6
    }
  }

Under the hood, Vue 2 uses Object.defineProperty to define all the data properties on a component instance, converting them to getters and setters. There's a deep dive into the Vue 2's reactivity system in the Vue.js docs that's worth spending some time with.

Because defining the properties happens at the time of the component instance's initialization, it results in some small drawbacks:

  • data properties cannot be added or deleted after component instance initialization. They have to be present during initialization for them to be reactive

  • If the data property is an array, it is not possible to set an item directly to the array through assignment by using the array index (as in arr[0] = value), and it also isn't possible to update the length of the array (as in arr.length = 0)

This isn't a major problem because the Vue.$set method can be used in cases where these updates need to be made after component instance initialization. However, Vue 3's reactivity system is so improved that now these issues are no longer a problem, making it unnecessary to use Vue.$set.

Reactivity in Vue 3

Vue 3's reactivity system had a major rewrite from what it was in Vue 2. The fundamental idea of tracking all the data properties and their dependencies so they can update automatically is still the same, but Vue 3 now uses the JavaScript Proxy API to achieve this (instead of Object.defineProperty like in Vue 2).

There's a rabbit hole to go down for anyone who wants to learn more about the Vue 3 reactivity system, and the Vue.js docs are fantastic. Even though I'm not going to explain it all (there's no way I could!), there are a few things I think are helpful to understand.

The docs state:

Proxy is an object that encases another object and allows you to intercept any interactions with that object.

Awareness of reactive properties using proxies is helpful when debugging code in the console. If I console.log a reactive property such as this property company:

const company = reactive({
  employees: ['Tom', 'Sara', 'Joe'],
  managers: ['Julie', 'Jorge'],
})

in the console, I see:

Clicking on it will open the object to show that there is a Handler and a Target. A proxy always contains a Handler and a Target, and since Vue 3 uses proxies, I find it helpful to be comfortable with the shape of this data as a proxy.

The Target is where to look for the actual values. It contains the data that I might be looking for. The Handler contains the special logic for making the data properties reactive. The Handler contains methods like get and set.

The Handler is the rabbit hole if you want to learn about reactivity. The Target is where I need to look for my data values.

Because reactive data is wrapped in a proxy, something to get used to when working with the data is the idea of having to 'unwrap' the data object to get to the value. After reading a lot of different resources about working with Vue 3 reactive data, I now feel comfortable with the idea that using strategies to 'unwrap' the data, such as destructuring or drilling down to the value property, are using the metaphor of unwrapping because Vue 3 reactive data is wrapped in a Proxy object.

How to make data properties reactive

As I said earlier, if I want to make data properties reactive in Vue 2, I have to return them in an object inside the data option of the Options API.

  data() {
    return {
      president: "Mickey Mouse",
      vicePresident: "Donald Duck"
    }
  }

If I am using the Vue 3 setup function (see my post on the setup function if you need an explainer on that), I can make data reactive by using the reactive or ref helpers.

ref

For this first example, I will use ref. I'm using ref because "Mickey Mouse" and "Donald Duck" are strings, and the recommendation is to use ref with primitive values (i.e. Javascript types that are not objects, such as strings, numbers, etc.)

First, I import ref:

<script>import { ref } from "vue";</script>

Then in the setup function, I set my variable to the ref() helper, which takes in the initial value. I must include the data in the return object if I want it to be available to the template.

  setup() {
    let president = ref("Mickey Mouse");
    let vicePresident = ref("Donald Duck");

    return { president, vicePresident };
    },

An important difference between ref and reactive is that if I want to do something to the value of my ref properties inside the setup function, I have to unwrap the object to access that value. So if I want to change the value of president, I will change president.value:

  function changePresident() {
    president.value = 'Goofy'
  }

I don't have to worry about unwrapping the values for president and vicePresident in the template. Vue can shallow unwrap those for me. 'Shallow unwrap' means the first level of properties in an object are available in the template without having to use .value (but nested properties would still need to be unwrapped).

<template>
  <div>
    <p><b>President:</b> {{ president }}</p>
    <p><b>Vice President:</b> {{ vicePresident }}</p>
  </div>
</template>

FYI, it's fine not to use ref if I don't need the data to be reactive, just writing the data like this:

setup() {
  let president = "Mickey Mouse"
  let vicePresident = "Donald Duck"

  return { president, vicePresident };
},

But it would mean the data isn't reactive, so I can't ever see updates to the data. If I use a method to change the data, I would never see that update change anything on the screen, and I would have to be happy with Mickey Mouse and Donald Duck showing as president and vice president forever.

There are times when you don't need the data to be reactive, so in those instances, just don't use ref or reactive!

reactive

I can use reactive for the same example, but I would only do so if I wanted the data to start out in the form of an object rather than separate string values. So in Vue 2, if I have this:

data() {
  return {
    executiveTeam: {
      president: "Mickey Mouse",
      vicePresident: "Donald Duck",
    },
  };
},

To change this to Vue 3 using reactive, I will first import reactive:

import { reactive } from 'vue'

In the setup function, I will create an object for executiveTeam and define the properties on the object. I can set the object to const since the object itself won't change, just the properties inside.

setup() {
  const executiveTeam = reactive({
    president: "Mickey Mouse",
    vicePresident: "Donald Duck",
  });

  return { executiveTeam };
},

And if I want to update the data, I do not have to unwrap it with .value like I do with ref.

function changePresident() {
  executiveTeam.president = 'Goofy'
}

This is because reactive is used with objects, and objects pass values by reference (which lends itself better to reactivity). Reactive references (ref) are used for primitive types, and primitives in Javascript pass values by value, so Vue has to wrap them in an object to make them reactive. Since ref properties are wrapped to make them reactive, they have to be unwrapped down to the .value to get the value. Read more about this concept in the Composition API RFC if this concept is something you want to understand more deeply.

However, because I'm returning the object executiveTeam and I want to access the properties president and vicePresident on that object in the template, I will have to drill down into the executiveTeam object to get each property I need:

<template>
  <div>
    <p><b>President:</b> {{ executiveTeam.president }}</p>
    <p><b>Vice President:</b> {{ executiveTeam.vicePresident }}</p>
  </div>
</template>

I cannot destructure the object that I return because if I do, the properties inside executiveTeam will lose reactivity. I'll demonstrate this in the next example to make this more clear.

When using reactive to give an object's properties reactivity, like this:

const executiveTeam = reactive({
  president: 'Mickey Mouse',
  vicePresident: 'Donald Duck',
})

I cannot destructure to try and return those properties by their key, as in:

//LOSES REACTIVITY:
let { president, vicePresident } = executiveTeam

return { president, vicePresident }

This is where toRefs comes in handy.

toRefs

The helper toRefs will allow me to turn each of the properties in the object into a ref, which means I won't have to use executiveTeam.president in the template; I'll be able to just write president. Here's the full example now using toRefs:

<script>
import { reactive, toRefs } from "vue";
export default {
  setup() {
    const executiveTeam = reactive({
      president: "Mickey Mouse",
      vicePresident: "Donald Duck",
    });

    //toRefs allows me to destructure
    let { president, vicePresident } = toRefs(executiveTeam);

    return { president, vicePresident };
  },
};
</script>

Since toRefs turns each property into a ref, I need to go back to unwrapping them down to their value using .value if I want to do something to them in the setup function:

function changePresident() {
  president.value = 'Goofy'
}

toRef

Just like toRefs, the helper toRef is used to turn reactive object properties into reactive references (ref), but I would use toRef if I just need to turn one property in a reactive object into a ref:

setup() {
  const executiveTeam = reactive({
    president: "Mickey Mouse",
    vicePresident: "Donald Duck",
  });
  //toRef used to turn just one property into a ref
  let presidentRef = toRef(executiveTeam, "president");

  const changePresident = () => {
    presidentRef.value = "Goofy";
  };

  return { presidentRef, changePresident };
},

I will have to use .value if I want to update the ref's value inside the setup function, but in the template, Vue will unwrap president for me:

<template>
  <div>
    <h1>Company Roles</h1>
    <p><b>President:</b> {{ presidentRef }}</p>
    <button @click="changePresident">Change President</button>
  </div>
</template>

It can be challenging to remember which variables are reactive properties and which ones are ref. Something that helps is to use a naming convention where I add the suffix Ref to anything that is a ref, such as presidentRef. I don't have a lot of experience with Vue 3 yet, but for the time being, I plan to use that naming convention to see if it helps me get a better handle on the distinction between ref and reactive properties.

isRef

Vue 3 also provides the helper isRef that I can use to check if something is a ref.

console.log(isRef(executiveTeam.president)) //false
console.log(isRef(presidentRef)) //true

My Thoughts on the Vue 3 Reactivity API

This topic of ref and reactive has been the most challenging for me in my goal to learn Vue 3. There is more nuance to how these helpers are used in practice, and it becomes too much for an introduction post to try to cover all the different situations where I might have to make informed decisions about using ref and/or reactive and all the other helpers.

The Vue.js team is aware that this is one of the challenges of Vue 3 - the question of when to use ref or reactive does not always receive a simple answer. In the Composition API RFC they state:

Understandably, users may get confused regarding which to use between ref and reactive. First thing to know is that you will need to understand both to efficiently make use of the Composition API. Using one exclusively will most likely lead to esoteric workarounds or reinvented wheels.

I have come across many resources that suggest using just ref or just reactive to start. But I think it is worth the effort to learn the nuances of both. I agree with the Vue.js team: it's better to understand both ref and reactive if I'm going to use Vue 3 to its fullest potential. And that's what I plan to do.

While using just ref for primitives and reactive for objects is one suggested approach (suggested by the Vue.js team here), I would encourage you to dig deeper into the docs and resources out there to learn more about the Reactivity APIs. For a nuanced feature such as this, it's important to understand why certain approaches can be taken.

Conclusion

Please join me for my next post about reusability in Vue 3, including a discussion of composition functions (i.e. Vue composables). In my opinion, composables are the best thing about this new Composition API and they make it worth putting in the time to learn the harder concepts of Vue 3.

Please reach out on Twitter and let me know if you are enjoying this series on Vue 3.

FEEDBACK