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...
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>
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>
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>
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:
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>
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>
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>
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.
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>
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.
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 })
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
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>
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.
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!