Vue 3 in depth

Vue 3 in depth

How senior Vue questions are asked, the mental model of the reactivity system and the component model, and how the pages in this section fit together.

Every Nuxt interview starts with Vue, and at senior level the questions stop being "what does computed do" and become "why does this component re-render", "why did this watcher not fire", "how would you design this API so the team cannot misuse it". The interviewer is checking that you understand the machine underneath the templates: a Proxy-based dependency tracker, a scheduler that batches effects, and a compiler that turns templates into render functions with patch flags.

This section covers Vue 3.5, which Nuxt 4 runs on, and names what Vue 3.6 changes. As of September 2026, 3.5.43 is the stable release and 3.6 is at release candidate 9; say "release candidate" in the room unless you have checked the tags that week.

Know

  • Reactivity is a Proxy plus an effect graph. reactive() wraps an object in a Proxy whose get trap records the running effect as a dependency and whose set trap schedules those effects. ref() is an object with a getter/setter on .value doing the same. computed is a lazy effect with a cache flag; watch and the render function are effects too.
  • Effects are batched. A mutation does not re-render synchronously: the scheduler queues the effect and flushes on the next microtask. Two mutations in one tick mean one render. nextTick() resolves after the flush, which is why tests await it.
  • Components are functions of state. A component instance owns a render effect. When a dependency read during the last render changes, only that component's render effect re-runs; children re-render only if their props changed (the compiler's patch flags and the shouldUpdateComponent check decide).
  • The compiler does the optimisation. Static hoisting, patch flags, block trees and cached event handlers mean a Vue template usually outperforms an equivalent hand-written render function. v-memo and v-once are the manual escape hatches.
  • Composition API is a design tool, not a syntax preference. It exists so that logic can be extracted, typed and reused as composables, and so that a component's concerns are grouped by feature rather than by option type. Options API still works and is not deprecated in 3.x; Nuxt 5 compiles Options API support out by default.
  • SSR changes the rules. Lifecycle hooks after setup (onMounted, onUpdated) never run on the server, window does not exist, and state must not be module-scoped or it leaks between requests. Every page in this section names the SSR angle because Nuxt makes it unavoidable.

How the pages fit

PageThe question it prepares you for
Reactivity system"Why did this not update?" and "What is the difference between ref, reactive and shallowRef?"
Component design"Design a component API for the team": props, emits, slots, defineModel, provide/inject, generics
Composables"How do you share logic?" and "What makes a composable SSR-safe?"
Rendering and lifecycleSuspense, Teleport, KeepAlive, async components, render functions, nextTick
Patterns and anti-patterns"Review this component": renderless components, prop drilling, watchers for derived state
API styles and migrationOptions vs Composition, mixins to composables, Vue 2 to 3, Nuxt 3 to 4
Vue 3.6 and Vapor"What is coming in Vue?": Vapor mode, alien-signals, what does not change

How it works

The whole reactivity system in thirty lines, which is worth being able to sketch on a whiteboard:

mini-reactivity.ts
type Effect = () => void
let activeEffect: Effect | null = null
const targetMap = new WeakMap<object, Map<PropertyKey, Set<Effect>>>()

function track(target: object, key: PropertyKey) {
  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: object, key: PropertyKey) {
  targetMap.get(target)?.get(key)?.forEach(effect => effect())
}

export function reactive<T extends object>(obj: T): T {
  return new Proxy(obj, {
    get(target, key, receiver) {
      track(target, key)
      return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
      const ok = Reflect.set(target, key, value, receiver)
      trigger(target, key)
      return ok
    },
  })
}

export function effect(fn: Effect) {
  const run = () => {
    activeEffect = run
    try { fn() }
    finally { activeEffect = null }
  }
  run()
}

Real Vue adds a scheduler (effects are queued and deduplicated instead of run synchronously), nested effect stacks, computed with dirty flags, array and collection instrumentation, and, in 3.6, a rewrite of the dependency graph on top of alien-signals. But the shape is exactly this, and being able to draw it is what turns "I know Vue" into "I understand Vue".

Gotcha· The Proxy is not the object

reactive(obj) !== obj. Code that holds the original and mutates it bypasses the trap, so nothing updates. The same trap catches people who spread a reactive object ({ ...state }) or destructure it (const { count } = state): the copies are plain values with no dependency tracking. toRefs exists for exactly that case.

Exercise

Exercise
  • Type the mini reactivity system above into a scratch file, add a computed() with a dirty flag and a microtask-batched scheduler, and prove with console.log that two mutations in one tick run the effect once.
  • Open a component in Vue DevTools, mutate a piece of state that the template does not read, and confirm no re-render happens. Then read it in the template and watch the render count change.

Be able to say

Be able to say· Explain Vue's reactivity system in one minute.

"Vue wraps state in Proxies. When an effect such as a component's render function runs, every property read goes through the get trap and Vue records that effect as a dependency of that property. When a set trap fires, the effects depending on that property are queued and flushed on the next microtask, so several mutations in one tick produce one re-render. ref does the same through a .value accessor, computed is a lazy, cached effect, and watch is an effect with a callback. The consequences are the things people trip on: a destructured or spread reactive object loses tracking, mutating the raw object bypasses the Proxy, and updates are asynchronous, which is why nextTick exists."

Your notes

Open in Notes

Nothing yet. Notes filed here show up on every page of this section.