Reactivity: ref and reactive
Introduction
Vue’s reactivity system tracks data changes and re-renders the DOM automatically. In Composition API, ref wraps any value (especially primitives) and reactive makes objects and arrays deeply reactive. This chapter shows when to use each, how template unwrapping works, and pitfalls like lost reactivity when destructuring.
Prerequisites
- Template Syntax and Directives
- Variables and data types in JavaScript
ref Basics
Code explanation:
ref(0)returns{ value: 0 }with reactive tracking- In
<script>, read/writecount.value - In
<template>, Vue auto-unwraps refs—usecount, notcount.value
Refs can hold any type:
const user = ref<{ name: string } | null>(null);
user.value = { name: "Ada" };reactive for Objects
Code explanation:
reactiveproxies nested objects—property changes trigger updates- No
.valuefor top-level properties onstate - Replacing
stateentirely breaks the reactive link—mutate properties or reassign viaObject.assign
ref vs reactive
| ref | reactive | |
|---|---|---|
| Best for | Primitives, reassign whole value | Plain object shape |
| Reassign | count.value = 5 works | Lose proxy if state = {} |
| Template | Auto-unwrap | Use object as-is |
| Destructure | Loses reactivity unless toRefs | Loses reactivity unless toRefs |
Rule of thumb: ref for most cases; reactive when you have a fixed object bag of fields.
Destructuring and toRefs
Wrong—breaks reactivity:
const state = reactive({ count: 0 });
let { count } = state;
count += 1; // not trackedFix with toRefs:
toRef(state, 'count') for a single property.
Replacing Arrays and Objects
Reactive array—prefer mutating methods:
const items = ref<string[]>(["a"]);
items.value.push("b");
items.value = [...items.value, "c"]; // new array also works with refreactive array:
const items = reactive(["a"]);
items.push("b");readonly and shallowRef (Concept)
readonly — prevent accidental mutations through proxy:
import { reactive, readonly } from "vue";
const original = reactive({ count: 0 });
const copy = readonly(original);
// copy.count++ throws in devshallowRef — only .value replacement triggers update (large immutable data, external libraries).
nextTick
DOM updates asynchronously after state change:
Code explanation:
nextTickwaits for Vue to flush DOM updates- Template refs (
ref="box") covered more in component chapters
Reactivity and JavaScript Closures
Event handlers and setTimeout close over refs correctly when you use .value:
const count = ref(0);
setTimeout(() => {
count.value += 1;
}, 1000);Composables build on this pattern—Composables. Related reading: Closures in JavaScript.
Debugging Reactivity
- Vue DevTools—inspect ref values
watchEffectto log changes (next chapter)- Ensure you did not strip reactivity with destructuring or
JSON.parsewithout re-wrapping
Practice Exercise
Add v-model on inputs (requires nothing extra for string refs in Vue 3).
FAQ
Why .value feels awkward?
Explicit for JavaScript; template sugar hides it. Composables return refs consistently.
reactive() on Map/Set?
Vue 3.5+ improved support; for simple state prefer plain objects or refs.
Is ref deeply reactive inside?
ref({ a: 1 }) deeply reactive on .value object.
Pinia vs ref?
Local component state → ref/reactive. Shared app state → Pinia (chapter 16).
Vue 2 reactivity differences?
Vue 2 used Object.defineProperty—array index assignment quirks. Vue 3 Proxy fixes most cases.
Lost update after await?
Ensure you mutate .value on the same ref instance, not a copied primitive.