Skip to the solution
MediumFrontend12 min read

How does Vue's reactivity system work?

1.2k views

The problem

Question presented to candidate: "When I change cart.qty in a Vue 3 component, the total on screen updates without me calling anything. Explain how Vue knows what to update, and what actually happens between my assignment and the DOM changing."

What a strong answer should cover:

  • reactive() returns a JavaScript Proxy; ref() wraps a value in an object whose .value getter and setter do the same job.
  • Reading a property inside a running effect calls track, which records "this effect depends on this key". Writing calls trigger, which re-runs only the effects that depend on that key.
  • The component render function, every computed and every watcher are effects; dependencies are collected dynamically on each run, so a branch that was not read is not a dependency.
  • Writes are batched: the scheduler queues each affected job once and flushes the queue in a microtask, so three synchronous writes cause one render.
  • Contrast with Vue 2 (Object.defineProperty, no tracking of added keys) and mention the 3.4 and 3.5 internal refactors.
  • Know where the system stops: destructured values, non-proxied built-ins, and identity differences between the proxy and the raw object.

Clarifying questions expected:

  • "Do you want the user-facing API, or the internals: track, trigger and the scheduler?"
  • "Are we talking about Vue 3 only, or should I contrast it with Vue 2?"

Code / implementation expected: Optional β€” a 20-line toy track/trigger with a Proxy, or a component that logs how many renders three writes cause.

  • reactivity

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

12 min read Β· 11 sections

Read the code

VueThree writes, one render: watching track, trigger and batching happen
<script setup>
import { reactive, computed, watch, onUpdated } from "vue";

// reactive() wraps the object in a Proxy: reads are tracked, writes trigger.
const cart = reactive({ price: 12, qty: 1, coupon: "" });

// computed() is an effect too: it re-runs only when price or qty change.
const total = computed(() => {
  console.log("computing total");
  return cart.price * cart.qty;
});

// A watcher is a third kind of effect, scheduled before the next render.
watch(() => cart.qty, (q, old) => console.log(`watch: qty ${old} -> ${q}`));

onUpdated(() => console.log("DOM patched"));

function addThree() {
  // Three synchronous writes: Vue queues ONE watcher run and ONE render.
  cart.qty++;
  cart.qty++;
  cart.qty++;
  console.log("handler done, DOM still says:", document.querySelector("#total").textContent);
}
</script>

<template>
  <p id="total">{{ cart.qty }} x {{ cart.price }} = {{ total }}</p>
  <button @click="addThree">Add three</button>
  <input v-model="cart.coupon" placeholder="coupon (not used by total)" />
</template>
A 30-line toy track/trigger built on Proxy (plain Node)
// A 30-line toy version of Vue's track/trigger, to show the idea (not Vue's real code).
const targetMap = new WeakMap(); // target -> (key -> Set of effects)
let activeEffect = null;

function track(target, key) {
  if (!activeEffect) return;
  let deps = targetMap.get(target);
  if (!deps) targetMap.set(target, (deps = new Map()));
  let dep = deps.get(key);
  if (!dep) deps.set(key, (dep = new Set()));
  dep.add(activeEffect);
}
function trigger(target, key) {
  const dep = targetMap.get(target)?.get(key);
  if (dep) [...dep].forEach((fn) => fn());
}
function reactive(obj) {
  return new Proxy(obj, {
    get(t, k, r) { track(t, k); return Reflect.get(t, k, r); },
    set(t, k, v, r) { const ok = Reflect.set(t, k, v, r); trigger(t, k); return ok; },
  });
}
function effect(fn) {
  const run = () => { activeEffect = run; try { fn(); } finally { activeEffect = null; } };
  run();
}

const cart = reactive({ price: 12, qty: 2, note: "gift" });
effect(() => console.log(`total = ${cart.price * cart.qty}`));
cart.qty = 3;        // total depends on qty, so the effect re-runs
cart.note = "none";  // nothing read note, so nothing re-runs
cart.price = 10;

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 18 of 92 in Vue.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Vue.js questions
Up nextMedium

What is Nuxt and how does it relate to Vue?

Continue