watch() Lazy Source vs watchEffect() Flush Timing (pre, post, sync)
- Last updated
- Prerequisites:
- Vue 3 Composition API
- Vue ref() and reactive()
- vue
- watch
- watchEffect
- flush
- reactivity
- lifecycle
Read at your depth
The practical view
`watch(source, callback)` is lazy by default: it tracks explicit dependencies, provides `(newValue, oldValue)`, and only runs when the source changes. `watchEffect(callback)` runs immediately on initialization, automatically tracks any reactive dependencies read inside its body, but does not provide the previous value. The `flush` option (`'pre'`, `'post'`, `'sync'`) controls when the watcher callback executes relative to DOM updates.
The same idea in other frameworks
`useEffect` with dependency array (similar to post-flush watcher).Angular `effect()` (similar to watchEffect) and `toObservable()`.`watch()` and `watchEffect()` with `flush: 'pre' | 'post' | 'sync'`.Svelte 5 `$effect()` (runs on microtask post-flush) and `$derived()`.Legacy vs modern
Explicit watch() vs Automatic watchEffect() with Post-DOM Flush
`watch` requires explicit sources and previous values; `watchEffect` auto-tracks and can wait for DOM updates with `flush: 'post'`.
watchEffect(() => { readDom(el.value); }); // ❌ Reads stale DOM before renderwatchPostEffect(() => { readDom(buttonRef.value); }); // ✅ NEW: Guaranteed updated DOMInterview gotchas
Context
Interviewers test your knowledge of Vue's render queue and DOM synchronization lifecycle.
The mechanical answer
- **`flush: 'pre'` (Default)**: Watcher callback runs before the component's DOM patch phase. Multiple state updates are batched into a single pre-render run. - **`flush: 'post'` (`watchPostEffect`)**: Watcher callback runs *after* the component's DOM patch phase. Use this whenever reading template refs or measuring DOM dimensions. - **`flush: 'sync'` (`watchSyncEffect`)**: Watcher runs immediately and synchronously the instant the ref/reactive property mutates, bypassing batching (use with caution as it can cause performance bottlenecks).
Trap
Accessing template refs inside a default `watchEffect` on initial run will yield `null` because the component has not mounted yet.
Context
Checks knowledge of getter source functions in `watch()`.
The mechanical answer
You **must pass a getter function**: `watch(() => state.user.name, (newName, oldName) => { ... })`. Passing the property directly (`watch(state.user.name, ...)` ) passes a static string or primitive, which Vue cannot track.
Trap
Passing `state.property` directly into `watch` throws a runtime warning and will never trigger the callback.