Vue 3 and Nuxt · Vue 3 Fundamentals

Reactivity: ref, reactive, computed and watch

Understand Vue's reactivity system: ref and reactive state, cached computed values, batched watchers, and why destructuring loses reactivity.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A shopping-cart model built from Vue's reactivity primitives, run directly in Node: a computed total that's cached until its dependencies change, a watcher that runs once for several changes, and the destructuring mistake that silently breaks reactivity, with the toRefs fix.

The idea

  • ref(value) holds any value in .value; reactive(object) makes an object deeply reactive through a proxy.
  • computed derives values; it's cached and recomputed only when a dependency changes and the value is read.
  • watch runs side effects when sources change. Callbacks are batched: several synchronous changes trigger one run (after nextTick).
  • Destructuring a reactive object copies plain values; use toRefs (or keep using state.x) to stay connected.

The code

Computed caching, batched watchers and destructuring
import { reactive, ref, computed, watch, nextTick, toRefs } from "vue";

const cart = reactive({ items: [{ name: "Mug", price: 8.5, qty: 2 }] });
const discount = ref(0);

const total = computed(() => {
  console.log("  (computing total)");
  const sum = cart.items.reduce((acc, item) => acc + item.price * item.qty, 0);
  return Number((sum * (1 - discount.value)).toFixed(2));
});

console.log("total:", total.value);
console.log("read again:", total.value);              // cached: no recomputation

watch(total, (now, before) => console.log(`watch: total ${before} -> ${now}`));
cart.items.push({ name: "Teapot", price: 24, qty: 1 });
discount.value = 0.1;
console.log("changed twice; watcher hasn't run yet");
await nextTick();                                      // the watcher runs once for both changes

const state = reactive({ count: 0 });
let { count } = state;                                 // a plain copy of the number
const { count: countRef } = toRefs(state);             // a ref linked to state.count
state.count = 5;
console.log(`destructured: ${count}, toRefs: ${countRef.value}`);

Output

  (computing total)
total: 17
read again: 17
changed twice; watcher hasn't run yet
  (computing total)
watch: total 17 -> 36.9
destructured: 0, toRefs: 5

Run this on your computer — see “Get Started” for setup.

Common mistakes

  • Destructuring reactive objects or props and losing reactivity; use toRefs, computed, or access through the object.
  • Replacing a whole reactive object (state = reactive({...})) instead of mutating it; use a ref when you need to replace the value.
  • Side effects inside computed; computed getters should be pure, use watch for effects.
  • Expecting watchers to run synchronously after each change; they're batched until the next tick.

Key points

  • ref for any value, reactive for objects; computed is cached.
  • Watchers are batched and run once per tick.
  • Destructuring breaks reactivity unless you use toRefs.

Further reading: Reactivity fundamentals · Computed properties · Watchers

Practice questions

What's the difference between ref and reactive?

Answer

ref wraps any value (accessed via .value, replaceable); reactive returns a deep proxy of an object, which can't be replaced wholesale and loses reactivity when destructured.

When does a computed value recompute?

Answer

Only when one of its reactive dependencies has changed and the value is read again; otherwise the cached value is returned.

Why did the watcher run once after two changes?

Answer

Watcher callbacks are batched and flushed asynchronously (before the next render by default), so several synchronous changes trigger one run.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?