Reactivity In Vue

Reactivity In Vue

In this article, we’re going to look at reactivity in Vue, how it works, and how we can create reactive variables using newly created methods and functions. This article is targeted at developers who have a good understanding of how Vue 2.x works and are looking to get familiar with the new Vue 3.

We’re going to build a simple application to better understand this topic. The code for this app can be found on GitHub.

By default, JavaScript isn’t reactive. This means that if we create the variable boy and reference it in part A of our application, then proceed to modify boy in part B, part A will not update with the new value of boy.

let framework = 'Vue';
let sentence = `${framework} is awesome`;
console.log(sentence)
 // logs "Vue is awesome"
framework = 'React';
console.log(sentence)
//should log "React is awesome" if 'sentence' is reactive.

The snippet above is a perfect example of the non-reactive nature of JavaScript — hence, why the change isn’t reflected in the sentence variable.

In Vue 2.x, props, computed, and data() were all reactive by default, with the exception of properties that are not present in data when such components are created. This means that when a component is injected into the DOM, only the existing properties in the component’s data object would cause the component to update if and when such properties change.

Internally, Vue 3 uses the Proxy object (an ECMAScript 6 feature) to ensure that these properties are reactive, but it still provides the option to use Object.defineProperty from Vue 2 for Internet Explorer support (ECMAScript 5). This method defines a new property directly on an object, or modifies an existing property on an object, and returns the object.

At first glance and since most of us already know that reactivity is not new in Vue, it might seem unnecessary to make use of these properties, but the Options API has its limitations when you’re dealing with a large application with reusable functions in several parts of the application. To this end, the new Composition API was introduced to help with abstracting logic in order to make a code base easier to read and maintain. Also, we can now easily make any variable reactive regardless of its data type using any of the new properties and methods.

When we use the setup option, which serves as the entry point for the Composition API, the data object, computed properties, and methods are inaccessible because the component instance has not yet been created when setup is executed. This makes it impossible to take advantage of the built-in reactivity in any of these features in setup. In this tutorial, we’re going to learn about all of the ways we can do this.

The Reactive Method

According to the documentation, the reactive method, which is the equivalent of Vue.observable() in Vue 2.6, can be useful when we’re trying to create an object all of whose properties are reactive (such as the data object in the Options API). Under the hood, the data object in the Options API uses this method to make all of the properties in it reactive.

But we can create our own reactive object like this:

import { reactive } from 'vue'

// reactive state
let user = reactive({
        "id": 1,
        "name": "Leanne Graham",
        "username": "Bret",
        "email": "Sincere@april.biz",
        "address": {
            "street": "Kulas Light",
            "suite": "Apt. 556",
            "city": "Gwenborough",
            "zipcode": "92998-3874",
            "geo": {
                "lat": "-37.3159",
                "lng": "81.1496"
            }
        },
        "phone": "1-770-736-8031 x56442",
        "website": "hildegard.org",
        "company": {
            "name": "Romaguera-Crona",
            "catchPhrase": "Multi-layered client-server neural-net",
            "bs": "harness real-time e-markets"
        },
        "cars": {
            "number": 0
        }
    })

Here, we imported the reactive method from Vue, and then we declared our user variable by passing its value to this function as an argument. In doing so, we’ve made user reactive, and, thus, if we use user in our template and if either the object or a property of this object should change, then this value will get automatically updated in this template.

ref

Just as we have a method for making objects reactive, we also need one to make other standalone primitive values (strings, booleans, undefined values, numbers, etc.) and arrays reactive. During development, we would work with these other data types while also needing them to be reactive. The first approach we might think of would be to use reactive and pass in the value of the variable that we want to make reactive.

import { reactive } from 'vue'

const state = reactive({
  users: [],
});

Because reactive has deep reactive conversion, user as a property would also be reactive, thereby achieving our goal; hence, user would always update anywhere it is used in the template of such an app. But with the ref property, we can make any variable with any data type reactive by passing the value of that variable to ref. This method also works for objects, but it nests the object one level deeper than when the reactive method is used.

let property = {
  rooms: '4 rooms',
  garage: true,
  swimmingPool: false
}
let reactiveProperty = ref(property)
console.log(reactiveProperty)
// prints {
// value: {rooms: "4 rooms", garage: true, swimmingPool: false}
// }

Under the hood, ref takes this argument passed to it and converts it into an object with a key of value. This means, we can access our variable by calling variable.value, and we can also modify its value by calling it in the same way.

import {ref} from 'vue'
let age = ref(1)

console.log(age.value)
//prints 1
age.value++
console.log(age.value)
//prints 2

With this, we can import ref into our component and create a reactive variable:

<template>
  <div class="home">
    <form @click.prevent="">
      <table>
        <tr>
          <th>Name</th>
          <th>Username</th>
          <th>email</th>
          <th>Edit Cars</th>
          <th>Cars</th>
        </tr>
        <tr v-for="user in users" :key="user.id">
          <td>{{ user.name }}</td>
          <td>{{ user.username }}</td>
          <td>{{ user.email }}</td>
          <td>
            <input
              type="number"
              style="width: 20px;"
              name="cars"
              id="cars"
              v-model.number="user.cars.number"
            />
          </td>
          <td>
            <cars-number :cars="user.cars" />
          </td>
        </tr>
      </table>
      <p>Total number of cars: {{ getTotalCars }}</p>
    </form>
  </div>
</template>
<script>
  // @ is an alias to /src
  import carsNumber from "@/components/cars-number.vue";
  import axios from "axios";
  import { ref } from "vue";
  export default {
    name: "Home",
    data() {
      return {};
    },
    setup() {
      let users = ref([]);
      const getUsers = async () => {
        let { data } = await axios({
          url: "data.json",
        });
        users.value = data;
      };
      return {
        users,
        getUsers,
      };
    },
    components: {
      carsNumber,
    },
    created() {
      this.getUsers();
    },
    computed: {
      getTotalCars() {
        let users = this.users;
        let totalCars = users.reduce(function(sum, elem) {
          return sum + elem.cars.number;
        }, 0);
        return totalCars;
    },
  };
</script>

Here, we imported ref in order to create a reactive users variable in our component. We then imported axios to fetch data from a JSON file in the public folder, and we imported our carsNumber component, which we’ll be creating later on. The next thing we did was create a reactive users variable using the ref method, so that users can update whenever the response from our JSON file changes.

We also created a getUser function that fetches the users array from our JSON file using axios, and we assigned the value from this request to the users variable. Finally, we created a computed property that computes the total number of cars that our users have as we have modified it in the template section.

It is important to note that when accessing ref properties that are returned in the template section or outside of setup(), they are automatically shallow unwrapped. This means that refs that are an object would still require a .value in order to be accessed. Because users is an array, we could simply use users and not users.value in getTotalCars.

In the template section, we displayed a table that displays each user’s information, together with a <cars-number /> component. This component accepts a cars prop that is displayed in each user’s row as the number of cars they have. This value updates whenever the value of cars changes in the user object, which is exactly how the data object or computed property would work if we were working with the Options API.

toRefs

When we use the Composition API, the setup function accepts two arguments: props and context. This props is passed from the component to setup(), and it makes it possible to access the props that the component has from inside this new API. This method is particularly useful because it allows for the destructuring of objects without losing its reactivity.

<template>
  <p>{{ cars.number }}</p>
</template>
<script>
  export default {
    props: {
      cars: {
        type: Object,
        required: true,
      },
      gender: {
        type: String,
        required: true,
      },
    },
    setup(props) {
      console.log(props);
   // prints {gender: "female", cars: Proxy}
    },
  };
</script>
<style></style>

To use a value that is an object from props in the Composition API while ensuring it maintains its reactivity, we make use of toRefs. This method takes a reactive object and converts it into a plain object in which each property of the original reactive object becomes a ref. What this means is that the cars prop…

cars: {
  number: 0
}

… would now become this:

{
  value: cars: {
    number: 0
  }

With this, we can make use of cars inside any part of the setup API while still maintaining its reactivity.

 setup(props) {
      let { cars } = toRefs(props);
      console.log(cars.value);
      // prints {number: 0}
    },

We can watch this new variable using the Composition API’s watch and react to this change however we might want to.

setup(props) {
      let { cars } = toRefs(props);
      watch(
        () => cars,
        (cars, prevCars) => {
          console.log("deep ", cars.value, prevCars.value);
        },
        { deep: true }
      );
    }

toRef

Another common use case we could be faced with is passing a value that is not necessarily an object but rather one of the data types that work with ref (array, number, string, boolean, etc.). With toRef, we can create a reactive property (i.e. ref) from a source reactive object. Doing this would ensure that the property remains reactive and would update whenever the parent source changes.

const cars = reactive({
  Toyota: 1,
  Honda: 0
})

const NumberOfHondas = toRef(state, 'Honda')

NumberOfHondas.value++
console.log(state.Honda) // 1

state.Honda++
console.log(NumberOfHondas.value) // 2

Here, we created a reactive object using the reactive method, with the properties Toyota and Honda. We also made use of toRef to create a reactive variable out of Honda. From the example above, we can see that when we update Honda using either the reactive cars object or NumberOfHondas, the value gets updated in both instances.

This method is similar and yet so different from the toRefs method that we covered above in the sense that it maintains its connection to its source and can be used for strings, arrays, and numbers. Unlike with toRefs, we do not need to worry about the existence of the property in its source at the time of creation, because if this property does not exist at the time that this ref is created and instead returns null, it would still be stored as a valid property, with a form of watcher put in place, so that when this value changes, this ref created using toRef would also be updated.

We can also use this method to create a reactive property from props. That would look like this:

<template>
  <p>{{ cars.number }}</p>
</template>
<script>
  import { watch, toRefs, toRef } from "vue";
  export default {
    props: {
      cars: {
        type: Object,
        required: true,
      },
      gender: {
        type: String,
        required: true,
      },
    },
    setup(props) {
      let { cars } = toRefs(props);
      let gender = toRef(props, "gender");
      console.log(gender.value);
      watch(
        () => cars,
        (cars, prevCars) => {
          console.log("deep ", cars.value, prevCars.value);
        },
        { deep: true }
      );
    },
  };
</script>

Here, we created a ref that would be based on the gender property gotten from props. This comes in handy when we want to perform extra operations on the prop of a particular component.

Conclusion

In this article, we have looked at how reactivity in Vue works using some of the newly introduced methods and functions from Vue 3. We started by looking at what reactivity is and how Vue makes use of the Proxy object behind the scenes to achieve this. We also looked at how we can create reactive objects using reactive and how to create reactive properties using ref.

Finally, we looked at how to convert reactive objects to plain objects, each of whose properties are a ref pointing to the corresponding property of the original object, and we saw how to create a ref for a property on a reactive source object.

Further Resources

  • “Proxy” (object), MDN Web Docs
  • “Reactivity Fundamentals”, Vue.js
  • “Refs”, Vue.js
  • “Lifecycle Hook Registration Inside setup”, Vue.js