Vue3: how to declare reactive states (en)

Angela Caldas - Mar 6 - - Dev Community

Ler em pt/br

In a Single Page Application (SPA), reactivity is the ability of the application to dynamically respond to changes in its data, updating parts of the page without the need to reload it from scratch.

When Vue detects changes in its states, it utilizes the virtual DOM to re-render the interface and ensure that it is always up-to-date, making the process as fast and efficient as possible.

There are different ways to declare reactive states in a Vue application, which may vary depending on the API in use (Options or Composition), as well as the data types themselves. That's what we'll cover in this article!

Table of Contents
Reactive data with Options API:
Data properties
Computed Properties
Difference between methods and computed properties
Reactive data with Composition API:
ref()
reactive()
reactive() limitations
computed()
Wrapping it up...

Chemical reaction


Reactive data with Options API

Data properties

With the Options API, we declare reactive states of a component through data properties, using the data method for that purpose. This method returns an object with all reactive states and their values.

<script>
export default {
  data () {
    counter: 0
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

In the example above, counter is a reactive data, and if modified, the interface will automatically update to reflect the changes.

To see reactivity in action, let's create a method (or function) that modifies the value of counter each time we click a button.

<script>
export default {
  data () {
    counter: 0
  },
  methods: {
    increaseCounter() {
      this.counter++
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

Counter updating

Note that, upon clicking the button, the value of counter is automatically updated in the interface.

It's important for all reactive data to be used in the component to be declared in the data option, even if they don't have a defined value yet, as they will be instantiated in the component as soon as it is created (allowing the use of this in methods and lifecycle hooks).

If you need to initialize a state without a defined value, you can use undefined, null, or any other value that serves as a placeholder (such as an empty string, for example).


Computed Properties

Computed properties are used when we need to perform calculations or other logic based on data properties in a passive manner. In other words, when a reactive data is changed, all computed properties that depend on that data are also updated. It's as if they were formatting functions for variables.

To declare computed properties, we use the function syntax within the computed option. This function should always return a value.

<script>
  export default {
    // hidden code
    computed: {
      doubleCounter() {
        return this.counter * 2
      }
    }
  }
</script>
Enter fullscreen mode Exit fullscreen mode

Note that we did not use the arrow function syntax in our computed property because they do not have this in their context, preventing us from accessing our data properties.

The computed property above always returns twice the value of counter. We can see in our application that whenever counter is modified, the value of doubleCounter is automatically updated:

doubleCounter updating

The same result could be achieved directly in the template of our component by inserting the logic within the interpolation ({{ }}, also called "mustache").

<template>
  <p>The double of counter is: {{ doubleCounter }}</p>
  <!-- would become -->
  <p>The double of counter is: {{ counter * 2 }}</p>
<template>
Enter fullscreen mode Exit fullscreen mode

This way, our application would behave the same. However, in larger applications, these logics can become much more complex, and with the use of computed properties, we can make our template more understandable and our code more organized.


Difference between methods and computed properties

It's easy to see that the same logic of doubleCounter could be achieved through a method instead of a computed property, right?

<template>
  <p>{{ doubleCounter() }}</p>
<template>

<script>
// hidden code
  methods: {
    doubleCounter() {
      return this.counter * 2
    }
  }
</script>
Enter fullscreen mode Exit fullscreen mode

With the code above, we could easily achieve the same result. And why didn't we choose this alternative? Because computed properties are cached based on the reactive data they depend on.

What does that mean?

It means that, as long as counter is not modified, doubleCounter will not change either and will always return the last computed result, regardless of how many times you re-render your component. On the other hand, methods will always be executed when the component is re-rendered, even if our reactive data counter does not change.

Why do we need caching? Imagine we have an expensive computed property list, which requires looping through a huge array and doing a lot of computations. Then we may have other computed properties that in turn depend on list. Without caching, we would be executing list’s getter many more times than necessary! — Extract from Vue docs.

So, let's use methods only when we don't need caching! 😉


Reactive data with Composition API

In Vue 3, reactivity is achieved through the system known as "Proxy-based Reactivity," which creates proxies around data objects, allowing the interception of read (get) and write (set) operations on the properties of these objects. This system is an evolution of the reactivity system used in previous Vue versions (which used Object.defineProperty), providing more efficient performance with fewer limitations.

As a result, with the Composition API we have different ways to declare reactive states. Here the data properties come out and the ref() and reactive() functions come into play!


ref()

To declare reactive data using the ref() function, we should use the variable declaration syntax with const. ref() is commonly used for states with primitive types (such as string, number, boolean, etc.). Let's use the same example of counter:

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

  const counter = ref(0);

  const increaseCounter = () => {
    counter.value++;
  };
</script>
Enter fullscreen mode Exit fullscreen mode

When we studied the differences between Options and Composition API, we saw that we need to import the native Vue functions we are going to use in our component. So, we start by importing ref and using it in our counter variable to encapsulate the initial value of our state.

To change the value of counter, we create an arrow function that modifies the .value property of our ref. This property is automatically created by the Vue 3 reactivity system, and it is not necessary to declare it when creating our state with ref(). This property allows Vue to detect whether a state has been accessed or modified.

Counter updating

Note that when we use our computed state directly in the template, there is no need to use the .value property.


reactive()

reactive() is the second way Vue provides for declaring reactive data. Unlike ref, which encapsulates the value of the data in an internal object with the .value property, the reactive() function makes object-type data reactive. Therefore, reactive() has the limitation of not accepting strings, numbers, and booleans.

Using our counter example, we can refactor our code to use reactive():

<script setup>
  import { reactive} from "vue";

  const state = reactive({ counter: 0 });

  const increaseCounter = () => {
    state.counter++;
  };
</script>
Enter fullscreen mode Exit fullscreen mode

In the code above, our variable state now receives an object { counter: 0 } as reactive data. In this case, if we want to change the value of counter, we can directly access the counter key within the state object in our increaseCounter method.

State counter updating

Note that in our template, we also need to access state.counter directly so that its value is correctly rendered on the screen.


reactive() limitations

As mentioned earlier, the first limitation of the reactive() function is not accepting string, number, and boolean. Therefore, this function should only be used for object-type data (such as objects themselves, arrays, and collections like Map and Set).

It's also not possible to reassign a new value to the same reactive object. In other words, you cannot replace the entire object with another object because the Vue reactivity system works directly with property access.

// It's not possible to reassign state
let state = reactive({ count: 0 })
state = reactive({ count: 1 })
Enter fullscreen mode Exit fullscreen mode

The use of reactive() also does not allow for destructuring because the reactivity connection reference is lost:

const state = reactive({ count: 0 })
// count is disconnected from state.count when destructured
let { count } = state
Enter fullscreen mode Exit fullscreen mode

computed()

The use of computed properties serves the same purpose as seen earlier: performing calculations and logic passively according to the detected change in a ref() or reactive().

However, with the Composition API, we have a different syntax for creating these computed properties: computed() is now a function that takes another function as a parameter and returns a computed ref. Let's look at an example:

<script setup>
import { ref, computed } from "vue";

// hidden code

const doubleCounter = computed(() => {
  return counter.value * 2;
});
</script>
Enter fullscreen mode Exit fullscreen mode

In the code above, we declare our computed property as a variable and use the computed() function to encapsulate the arrow function containing our logic.

doubleCounter updating


Wrapping it up...

We've explored the nuances of creating reactive data in Vue, diving into the syntax of the Options API and Composition API. When exploring the creation of data and computed properties, we've touched the essence of the Options API, where familiarity with data properties and computed properties proves crucial. Additionally, we've ventured into the Composition API, where ref, reactive and computed offer powerful tools for structuring reactive logic in a more modular and concise way.

Now that you know how to declare reactive data, expanding your repertoire to handle reactivity in Vue and build more robust and efficient applications, you're ready to take your Vue development experience to new heights. Experiment, innovate and enjoy the reactive journey!

. . . . . . . . . . . . . . . . . . . . . .