In this Vue.js tutorial, we are going to see how reactivity works through the ref and reactive functions.
Reactivity is a programming paradigm that allows us to have changes in data automatically reflected in the user interface, creating a connection between the data and the UI.
For example, if you have a counter in your application and you increase its value, Vue takes care of updating the number displayed on the screen without the need to manually manipulate the DOM.
That is, when you modify a reactive property, Vue will automatically detect that change and update any part of the interface that depends on that property.
Reactivity is one of the fundamental pillars of any modern framework. In Vue 3, it is primarily handled through two functions
- Use ref → For simple values
- Use reactive → For objects and arrays
Both allow us to create reactive data, but they are used in different contexts. Let’s look at them in detail.
Reactivity for Primitive Values with ref
The ref() function allows us to create reactive references to primitive values like strings, numbers, or booleans (although it can also contain objects).
The basic syntax of ref is as follows:
import { ref } from 'vue';
const count = ref(0); // Create a reactive reference with an initial value of 0
When we create a variable with ref(), Vue wraps the value in an object within a .value property. So to access or modify this value, we must use this property:
// Access the value
console.log(counter.value) // 0
// Modify the value
counter.value++
console.log(counter.value) // 1
It’s common to forget to use .value when starting with Vue.
However, in Vue templates we don’t need to use .value. Vue handles it automatically for us.
<template>
<p>The counter is: {{ count }}</p>
<button @click="count++">Increment</button>
</template>
Here we access the value of count without needing to use .value because Vue handles it for us.
Reactivity for Objects and Arrays with reactive
The reactive() function allows us to create complete reactive objects, where all its properties are automatically reactive. It is designed to handle objects and arrays.
The basic syntax of reactive is as follows:
import { reactive } from 'vue';
const state = reactive({
counter: 0,
message: 'Hello, LuisLlamas.es!'
});
With reactive, we can access and modify the object’s properties directly, without needing to use .value:
console.log(state.counter); // Access the value (0)
state.counter++; // Increment the value
console.log(state.counter); // Now it's 1
When to Use ref and When to Use reactive
The million-dollar question: When to use ref and when to use reactive? In summary:
- ✔️ Use it for primitive values (number, string, boolean, etc.)
- ✔️ Stores the value in a
.valueproperty - ✔️ Makes any type (even objects) reactive, but within a “wrapper” (Ref)
const name = ref('Ana')
const counter = ref(0)
const active = ref(true)
- ✔️ Use it for objects, arrays, maps, sets, etc.
- ✔️ Converts an entire object into reactive (each property becomes reactive).
- ✔️ If you nest objects inside, they will also be reactive (deep reactivity).
- ❌ Does not work for primitive values (it will return a “normal” value)
const user = reactive({
name: 'Ana',
age: 28,
address: {
street: 'Principal',
city: 'Barcelona'
},
preferences: ['music', 'cinema']
})
They are actually more equivalent than they seem. If we use ref with objects, Vue is smart enough to use reactive inside value.
So ref retains the same reactive capabilities as reactive. We simply have the (small) inconvenience of using value to access the object.
Deep Reactivity
By deep reactivity we mean whether the internal properties will also be reactive (e.g., obj.name, obj.details).
Both ref and reactive provide deep reactivity. That is, both of these cases will work correctly.
<script setup>
import { ref } from 'vue'
const obj = ref({
name: 'Vue',
details: {
version: 3
}
})
function changeName() {
obj.value.name = 'Vue.js' // ✅ Reactive
}
function changeVersion() {
obj.value.details.version = 4 // ✅ Reactive
}
</script>
<template>
<div>
<p>{{ obj.name }}</p>
<button @click="changeName">Change Name</button>
<p>{{ obj.details.version }}</p>
<button @click="changeVersion">Change Version</button>
</div>
</template>
We just have to remember to use value.
<script setup>
import { reactive } from 'vue'
const obj = reactive({
name: 'Vue',
details: {
version: 3
}
})
function changeName() {
obj.name = 'Vue.js' // ✅ Reactive
}
function changeVersion() {
obj.details.version = 4 // ✅ Also reactive
}
</script>
<template>
<div>
<p>{{ obj.name }}</p>
<button @click="changeName">Change Name</button>
<p>{{ obj.details.version }}</p>
<button @click="changeVersion">Change Version</button>
</div>
</template>
Shallow Versions
However, deep reactivity has a significant performance cost, and we won’t always need it. For this, we have shallowRef and shallowReactive, which are shallow versions (i.e., without deep reactivity).
- Similar to
ref, but does not make the value deep if it’s an object. - Only the directly assigned value is observed, not its internal properties.
import { shallowRef } from 'vue'
const user = shallowRef({
name: 'Alice'
})
user.value.name = 'Bob' // Does not trigger reactivity automatically
user.value = { name: 'Charlie' } // Changing the object does trigger it
- Same as
reactive, but only makes the first layer of the object reactive. - Nested objects are NOT reactive.
import { shallowReactive } from 'vue'
const state = shallowReactive({
count: 0,
user: {
name: 'Alice'
}
})
// state.count is reactive
// state.user.name is NOT reactive
Destructuring ref or reactive
Destructuring either ref or reactive directly will cause problems because it will break the reactivity system (in fact, it’s a common mistake ⚠️).
const count = ref(10)
const { value } = count
valuehere is a static copy, no longer linked tocount.- It is no longer reactive, and changing
count.valuewill not updatevalue, nor vice versa.
const state = reactive({ count: 10, name: 'Alice' })
const { count, name } = state
countandnameare now plain copies, they are no longer reactive.
Instead, we can use the following functions, which allow us to destructure or convert between the different types while maintaining reactivity.
| Function | Description |
|---|---|
toRef() | Converts a property of a reactive object into a ref (useful for destructuring). |
toRefs() | Converts all properties of a reactive object into refs. |
unref() | Gets the real value of a ref automatically (ref.value, but more elegant). |
The toRefs() function converts a reactive object into a plain object where each property is an individual reference:
import { reactive, toRefs } from 'vue'
const state = reactive({
name: 'Ana',
age: 28
})
// Convert to individual refs
const { name, age } = toRefs(state)
// Now we can use name.value and maintain reactivity
name.value = 'Carlos' // Also updates state.name
Similar to toRefs(), but for a single property:
import { reactive, toRef } from 'vue'
const state = reactive({ counter: 0 })
const counterRef = toRef(state, 'counter')
counterRef.value++ // Also increments state.counter
We can reconstruct reactive objects from individual refs:
import { ref, reactive } from 'vue'
const name = ref('Ana')
const age = ref(28)
// Create a reactive object with refs
const user = reactive({
name,
age
})
name.value = 'Carlos' // Also updates user.name
user.age = 29 // Also updates age.value
readonly Versions
Sometimes we will want to create an immutable version (read-only) of a reactive value (ref or reactive), preventing its direct modification. For example, this is useful if we want to protect data that should not be modified from certain components or functions.
For this, we have the readonly and shallowReadonly versions.
import { reactive, readonly } from 'vue'
const state = reactive({ count: 0 })
const readonlyState = readonly(state)
readonlyState.count++ // ❌ Error (in dev mode): Cannot assign to read only property
readonlyStatecan be read but not modified- If someone tries to modify it, Vue will give a warning in development, but will not stop execution in production
- It is still reactive
readonly() does not freeze the object like Object.freeze — it is still reactive internally, it only prevents external writes.
Internal Functioning Advanced
In case you are ever interested in delving a bit deeper into understanding how reactivity and the “magic” that Vue.js does works, let’s briefly see how it works internally.
Vue 3 implements a reactivity system based on JavaScript Proxies, which was a major improvement over the Object.defineProperty()-based system of Vue 2.
Basically,
- When using reactive variables, Vue creates a Proxy around the original object.
- During rendering or effect execution, Vue registers which properties are accessed.
- When a property changes, Vue notifies all effects that depend on that property.
That is, if we saw the process (very simplified) it would be something like the following.
// This is a conceptual simplification, not actual Vue code
function createReactive(obj) {
return new Proxy(obj, {
get(target, key) {
// Register that someone is accessing this property
track(target, key)
return target[key]
},
set(target, key, value) {
const oldValue = target[key]
target[key] = value
// Notify effects if the value changed
if (oldValue !== value) {
trigger(target, key)
}
return true
}
})
}
Vue.js performs these actions, along with tracking the dependencies of each object tracking and a view update mechanism trigger, to make everything comfortable for us to use.
In more technical terms, we can define reactivity as a system that allows us to track dependencies between variables and execute side effects when these variables change.
On the other hand, the need for .value is due to JavaScript’s limitations regarding reactivity of primitive values. Unlike objects, primitive values are passed by value and not by reference, which means Vue could not directly track their changes.
That’s why ref is a wrapper for a primitive value, which allows wrapping the variable in an object, enabling Vue.js’s reactivity system to do its work.
