Skip to the solution
MediumFrontend10 min read

watch vs watchEffect — flush timing and the new watch options in 3.5?

313 views

The problem

Question presented to candidate: "Forget the basic difference between watch and watchEffect for a moment. When exactly does a watcher callback run relative to the component re-render, what do flush: 'pre', 'post' and 'sync' change, and which watch options and APIs are new in Vue 3.5?"

What a strong answer should cover:

  • Writes are batched: pre watchers (the default) run in the scheduler flush before the component re-renders, so they see the new state but the old DOM.
  • flush: 'post' runs after the DOM patch (alongside onUpdated), so it can read the updated DOM; watchPostEffect is the shorthand.
  • flush: 'sync' runs synchronously on every write, with no batching; watchSyncEffect is the shorthand. Use it rarely.
  • New in 3.5: deep accepts a number (traversal depth), the handle has pause() / resume(), and onWatcherCleanup() registers cleanup without the third callback argument. getCurrentWatcher() and a base watch in @vue/reactivity landed too.
  • once: true is from 3.4, not 3.5; immediate and boolean deep predate both.
  • Know the edges: onWatcherCleanup must be called before the first await; resuming a paused watcher fires once if the source changed while paused.

Clarifying questions expected:

  • "Does the callback need to read the updated DOM, or only the new state?"
  • "Is the project on 3.5 already? Some of these options do not exist on 3.4."

Code / implementation expected: Yes — a component with sync, pre and post watchers on the same ref, logging what the DOM shows in each, plus a short script exercising the 3.5 options.

  • watch
  • watcheffect
  • flush

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

10 min read · 9 sections

Read the code

Vuesync, pre and post watchers on one ref, logging what the DOM shows
<script setup>
import { ref, watch, watchEffect, onBeforeUpdate, onUpdated, useTemplateRef } from "vue";

const count = ref(0);
const p = useTemplateRef("p");
const dom = () => p.value?.textContent; // what the user can see right now

// Same source, three flush modes. Each callback logs what the DOM shows at that moment.
watch(count, (v) => console.log(`sync  watcher: count=${v}, DOM says "${dom()}"`), { flush: "sync" });
watch(count, (v) => console.log(`pre   watcher: count=${v}, DOM says "${dom()}"`)); // default: flush "pre"
watch(count, (v) => console.log(`post  watcher: count=${v}, DOM says "${dom()}"`), { flush: "post" });
watchEffect(() => console.log(`watchEffect (pre): count=${count.value}`));

onBeforeUpdate(() => console.log(`onBeforeUpdate: DOM says "${dom()}"`));
onUpdated(() => console.log(`onUpdated: DOM says "${dom()}"`));

function bumpTwice() {
  console.log("click: count++ (1st)");
  count.value++;
  console.log("click: count++ (2nd)");
  count.value++;
  console.log("click: handler returns");
}
</script>

<template>
  <p ref="p">count is {{ count }}</p>
  <button @click="bumpTwice">+2</button>
</template>
once (3.4), deep: number, pause/resume and onWatcherCleanup (3.5) in Node
import { ref, reactive, watch, watchEffect, nextTick, onWatcherCleanup } from "vue";
const tick = () => nextTick();

console.log("--- deep: number (3.5) limits traversal depth");
const tree = reactive({ a: { b: { c: 1 } }, top: 1 });
watch(tree, () => console.log("  deep: true     fired"), { deep: true });
watch(tree, () => console.log("  deep: 1        fired"), { deep: 1 });
watch(tree, () => console.log("  deep: 2        fired"), { deep: 2 });
tree.top++;      await tick(); console.log("  (changed tree.top: depth 1)");
tree.a.b.c++;    await tick(); console.log("  (changed tree.a.b.c: depth 3)");

console.log("--- once: true (added in 3.4)");
const q = ref(0);
watch(q, (v) => console.log("  once fired with", v), { once: true });
q.value = 1; await tick();
q.value = 2; await tick();
console.log("  (second change ignored)");

console.log("--- pause() / resume() on the handle (3.5)");
const temp = ref(20);
const h = watch(temp, (v, old) => console.log(`  watcher: ${old} -> ${v}`));
h.pause();
temp.value = 21; await tick();
temp.value = 22; await tick();
console.log("  (paused: nothing logged for 21, 22)");
h.resume(); await tick();
temp.value = 23; await tick();

console.log("--- pause/resume on watchEffect");
const e = ref(0);
const h2 = watchEffect(() => console.log("  effect sees", e.value));
h2.pause(); e.value = 5; await tick();
console.log("  (paused)");
h2.resume(); await tick();

console.log("--- onWatcherCleanup (3.5) cancels the previous run");
const id = ref(1);
watch(id, (v) => {
  const timer = setTimeout(() => console.log(`  loaded user ${v}`), 20);
  console.log(`  start loading user ${v}`);
  onWatcherCleanup(() => { clearTimeout(timer); console.log(`  cancelled user ${v}`); });
});
id.value = 2; await tick();
id.value = 3; await tick();
await new Promise((r) => setTimeout(r, 40));

console.log("--- the older 3rd-argument onCleanup still works");
const id2 = ref(1);
watch(id2, (v, _old, onCleanup) => { onCleanup(() => console.log(`  cleanup for ${v}`)); });
id2.value = 2; await tick(); id2.value = 3; await tick();

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 64 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

useAttrs() vs $attrs — how do you handle fallthrough attributes in composition API?

Continue