How does Vue track state changes and trigger re-renders internally?

LowHardVue
Preparing for interviews?

Use guided tracks for structured prep, then practice company-specific question sets when you want targeted interview coverage.

Quick Answer

Vue re-renders components by combining (1) reactive state (Proxy-based in Vue 3), (2) dependency tracking (track/trigger), and (3) a scheduler that batches component updates, then patches the DOM via virtual DOM diffing (optimized with compiler hints like patch flags).

Answer

Core idea

Vue doesn’t “poll” your state. It records who read what during render, and when that state changes it re-runs only the affected render effects.

Step

What happens internally

Why it matters

1) Make state reactive

Vue wraps objects with a Proxy (Vue 3) so it can intercept get/set.

Lets Vue observe reads/writes without you calling setState.

2) Render inside an effect

Each component render runs inside a reactive “effect” (think: tracked function).

Any reactive reads during render become dependencies.

3) Track dependencies on reads

On get, Vue calls track(target, key) to link (target,key) → activeEffect.

Vue learns exactly which component depends on which property.

4) Trigger on writes

On set, Vue calls trigger(target, key) to find all effects depending on (target,key).

Only affected components/computed/watchers are scheduled.

5) Batch updates

Component effects are queued (deduped) and flushed in a microtask (scheduler).

Multiple synchronous mutations cause only one re-render per component.

6) Re-render + patch DOM

Re-run render → new VNode tree → patch(oldVNode, newVNode) updates DOM.

Virtual DOM diff updates only what changed.

Vue’s internal reactivity → rendering pipeline

Dependency tracking (track/trigger) in simplified pseudo-code

JAVASCRIPT
// Highly simplified mental model (not Vue source)
const targetMap = new WeakMap();
let activeEffect = null;

function track(target, key) {
  if (!activeEffect) return;
  let depsMap = targetMap.get(target);
  if (!depsMap) targetMap.set(target, (depsMap = new Map()));
  let dep = depsMap.get(key);
  if (!dep) depsMap.set(key, (dep = new Set()));
  dep.add(activeEffect);
}

function trigger(target, key) {
  const depsMap = targetMap.get(target);
  const dep = depsMap?.get(key);
  if (!dep) return;
  dep.forEach(effect => effect.scheduler ? effect.scheduler(effect) : effect.run());
}

function effect(fn, scheduler) {
  const e = {
    run() {
      activeEffect = e;
      try { return fn(); } finally { activeEffect = null; }
    },
    scheduler
  };
  e.run();
  return e;
}
                  

How “re-render” is actually wired

Each component has a render effect. When its dependencies trigger, Vue doesn’t immediately run it; it queues it.

JAVASCRIPT
// Conceptually
const componentUpdateEffect = effect(
  () => {
    const nextVNodeTree = renderComponent();
    patch(prevVNodeTree, nextVNodeTree);
    prevVNodeTree = nextVNodeTree;
  },
  (job) => queueJob(job) // scheduler batching
);
                  

Scheduler / batching

Vue queues component jobs and flushes them in a microtask. So if you do state.a++ then state.b++ synchronously, the component typically renders once. nextTick() resolves after the queued DOM patches are applied.

Vue 3 render performance extra

Vue 3’s compiler adds hints (patch flags + block tree) so the runtime can skip diffing stable parts of the template and focus on dynamic bindings. Reactivity decides which components update; patch flags help decide how much work the DOM patch does within that update.

Reactive primitive

Internal behavior

Update timing

computed()

Runs as a lazy effect; caches value until a dependency triggers (marks it “dirty”).

Recomputes on next access, not immediately on every change.

watch()

Creates an effect on a getter; on trigger runs a callback (side effects).

Flush can be pre/post/sync (controls when callback runs relative to render).

ref()

Wraps a value; reads/writes track/trigger via .value.

Same scheduler rules when used by components.

Where computed/watch/ref fit in the same engine

Practical implications / common gotchas

• If you destructure reactive objects (e.g., const { x } = reactiveObj), you may lose tracking because the render no longer reads through the Proxy.
• If you mutate many things in a row, expect one render (batched), and use await nextTick() when you need DOM to be updated.
• If something doesn’t update, it usually means the render didn’t read that reactive source (no dependency tracked).

Summary

Vue tracks reactive reads during render (track), schedules dependent component effects on writes (trigger → queue), then re-renders and patches the DOM in a batched flush. That’s the whole “state change → re-render” loop.

Similar questions
Guides
17 / 34