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

ref Basics

Code explanation:

  • ref(0) returns { value: 0 } with reactive tracking
  • In <script>, read/write count.value
  • In <template>, Vue auto-unwraps refs—use count, not count.value

Refs can hold any type:

typescript
const user = ref<{ name: string } | null>(null);
user.value = { name: "Ada" };

reactive for Objects

Code explanation:

  • reactive proxies nested objects—property changes trigger updates
  • No .value for top-level properties on state
  • Replacing state entirely breaks the reactive link—mutate properties or reassign via Object.assign

ref vs reactive

refreactive
Best forPrimitives, reassign whole valuePlain object shape
Reassigncount.value = 5 worksLose proxy if state = {}
TemplateAuto-unwrapUse object as-is
DestructureLoses reactivity unless toRefsLoses 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:

typescript
const state = reactive({ count: 0 });
let { count } = state;
count += 1; // not tracked

Fix with toRefs:

toRef(state, 'count') for a single property.

Replacing Arrays and Objects

Reactive array—prefer mutating methods:

typescript
const items = ref<string[]>(["a"]);
 
items.value.push("b");
items.value = [...items.value, "c"]; // new array also works with ref

reactive array:

typescript
const items = reactive(["a"]);
items.push("b");

readonly and shallowRef (Concept)

readonly — prevent accidental mutations through proxy:

typescript
import { reactive, readonly } from "vue";
 
const original = reactive({ count: 0 });
const copy = readonly(original);
// copy.count++ throws in dev

shallowRef — only .value replacement triggers update (large immutable data, external libraries).

nextTick

DOM updates asynchronously after state change:

Code explanation:

  • nextTick waits 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:

typescript
const count = ref(0);
 
setTimeout(() => {
  count.value += 1;
}, 1000);

Composables build on this pattern—Composables. Related reading: Closures in JavaScript.

Debugging Reactivity

  1. Vue DevTools—inspect ref values
  2. watchEffect to log changes (next chapter)
  3. Ensure you did not strip reactivity with destructuring or JSON.parse without 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.