Reactivity Fundamentals
Vue.js Reactivity Fundamentals
Section titled “Vue.js Reactivity Fundamentals”Vue.js offers a powerful reactivity system that automatically updates the DOM when underlying data changes. This document covers key concepts of Vue’s reactivity system.
Declaring Reactive State
Section titled “Declaring Reactive State”Using ref()
Section titled “Using ref()”In Composition API, the recommended way to declare reactive state is using the ref() function:
import { ref } from 'vue'const count = ref(0)ref() takes the argument and returns it wrapped within a ref object with a .value property:
const count = ref(0)console.log(count) // { value: 0 }console.log(count.value) // 0
// Modifying the valuecount.value++console.log(count.value) // 1Using in Components
Section titled “Using in Components”To access refs in a component’s template, declare and return them from a component’s setup() function:
import { ref } from 'vue'
export default { // `setup` is a special hook dedicated for the Composition API. setup() { const count = ref(0)
// expose the ref to the template return { count } }}In the template:
<div>{{ count }}</div>Important: Notice that we do not need to append .value when using the ref in the template. For convenience, refs are automatically unwrapped when used inside templates.
Using <script setup>
Section titled “Using <script setup>”With Single-File Components (SFCs), you can use the simpler <script setup> syntax:
<script setup>import { ref } from 'vue'const count = ref(0)function increment() { count.value++}</script>
<template> <button @click="increment"> {{ count }} </button></template>Top-level imports, variables, and functions declared in <script setup> are automatically usable in the template of the same component.
Why Refs?
Section titled “Why Refs?”You might wonder why we need refs with the .value property instead of plain variables. This is related to how Vue’s reactivity system works:
- When you use a ref in a template and change its value later, Vue automatically detects the change and updates the DOM.
- This is made possible with a dependency-tracking based reactivity system.
- When a component renders for the first time, Vue tracks every ref that was used during the render.
- Later, when a ref is mutated, it will trigger a re-render for components that are tracking it.
In standard JavaScript, there’s no way to detect access or mutation of plain variables. However, we can intercept the get and set operations of an object’s properties using getter and setter methods.
Conceptually, a ref looks like this:
// pseudo code, not actual implementationconst myRef = { _value: 0, get value() { track() return this._value }, set value(newValue) { this._value = newValue trigger() }}Using reactive()
Section titled “Using reactive()”Another way to declare reactive state is with the reactive() API:
import { reactive } from 'vue'const state = reactive({ count: 0 })Usage in template:
<button @click="state.count++"> {{ state.count }}</button>Reactive vs. Ref
Section titled “Reactive vs. Ref”reactive()makes an object itself reactive, whileref()wraps the inner value in a special object.reactive()creates a proxy of the original object.reactive()converts the object deeply - nested objects are also wrapped withreactive().
Limitations of reactive()
Section titled “Limitations of reactive()”-
Limited value types: It only works for object types (objects, arrays, and collection types like
MapandSet). It cannot hold primitive types likestring,number, orboolean. -
Cannot replace entire object: Since Vue’s reactivity tracking works over property access, we must always keep the same reference to the reactive object.
let state = reactive({ count: 0 })// the above reference ({ count: 0 }) is no longer trackedstate = reactive({ count: 1 }) -
Not destructure-friendly: When we destructure a reactive object’s primitive type property into local variables, or pass that property into a function, we lose the reactivity connection.
const state = reactive({ count: 0 })// count is disconnected from state.count when destructuredlet { count } = state// does not affect original statecount++
Due to these limitations, Vue recommends using ref() as the primary API for declaring reactive state.
Deep Reactivity
Section titled “Deep Reactivity”Refs can hold any value type, including deeply nested objects, arrays, or JavaScript built-in data structures like Map.
A ref makes its value deeply reactive. This means changes are detected even when you mutate nested objects or arrays:
import { ref } from 'vue'
const obj = ref({ nested: { count: 0 }, arr: ['foo', 'bar']})
function mutateDeeply() { // these will work as expected. obj.value.nested.count++ obj.value.arr.push('baz')}Non-primitive values are turned into reactive proxies via reactive(), which is called by ref() internally when the ref value is an object.
Additional Ref Unwrapping Details
Section titled “Additional Ref Unwrapping Details”As Reactive Object Property
Section titled “As Reactive Object Property”A ref is automatically unwrapped when accessed or mutated as a property of a reactive object:
const count = ref(0)const state = reactive({ count})
console.log(state.count) // 0
state.count = 1console.log(count.value) // 1If a new ref is assigned to a property linked to an existing ref, it will replace the old ref:
const otherCount = ref(2)state.count = otherCountconsole.log(state.count) // 2// original ref is now disconnected from state.countconsole.log(count.value) // 1Caveat in Arrays and Collections
Section titled “Caveat in Arrays and Collections”Unlike reactive objects, there is no unwrapping performed when the ref is accessed as an element of a reactive array or a native collection type like Map:
const books = reactive([ref('Vue 3 Guide')])// need .value hereconsole.log(books[0].value)
const map = reactive(new Map([['count', ref(0)]]))// need .value hereconsole.log(map.get('count').value)Caveat when Unwrapping in Templates
Section titled “Caveat when Unwrapping in Templates”Ref unwrapping in templates only applies if the ref is a top-level property in the template render context:
const count = ref(0)const object = { id: ref(1) }In the template:
{{ count + 1 }}works as expected{{ object.id + 1 }}does NOT work, becauseobject.idis not unwrapped
To fix this, we can destructure the ref into a top-level property:
const { id } = objectThen {{ id + 1 }} will work correctly.
A ref does get unwrapped if it is the final evaluated value of a text interpolation (a {{ }} tag), so {{ object.id }} will render 1 and is equivalent to {{ object.id.value }}.
DOM Update Timing
Section titled “DOM Update Timing”When you mutate reactive state, the DOM is updated automatically. However, DOM updates are not applied synchronously. Instead, Vue buffers them until the “next tick” in the update cycle to ensure each component updates only once no matter how many state changes you have made.
To wait for the DOM update to complete after a state change, you can use the nextTick() global API:
import { nextTick } from 'vue'
async function increment() { count.value++ await nextTick() // Now the DOM is updated}