Explain the difference between watch() and watchEffect() in Vue 3, when each runs, how dependency tracking works, why a watch callback can recurse by mutating its own source, and how watchEffect suppresses a direct synchronous self-trigger.
Frontend interview practice question
watch vs watchEffect in Vue: what’s the difference, when does each run, and how can you accidentally create infinite loops?
Interview quick answer
Interview focus
This Vue interview question tests whether you can explain should you use watch vs watchEffect in Vue, connect it to production trade-offs, and handle common follow-up questions.
- should you use watch vs watchEffect in Vue explanation without falling back to memorized definitions
- Watch and Effects reasoning, edge cases, and production failure modes
- How you would answer the most likely Vue interview follow-up
Use this Vue interview question to rehearse a quick answer, common mistake, follow-up, and production pitfall.
Full interview answer
Overviewwatch() and watchEffect() both run side effects in response to reactive changes, but they work differently: watch is explicit and lazy (you choose the source), while watchEffect is automatic and eager (Vue tracks reactive values read during its synchronous execution). A watch() callback can recurse if it writes to its own source. Vue suppresses a direct synchronous self-trigger from watchEffect, but effects should still avoid mutating their dependencies because external writes, indirect cycles, and async work make the flow difficult to reason about.
Aspect | watch() | watchEffect() |
|---|---|---|
Dependency tracking | Explicit (you pass the source) | Automatic (tracks what you read inside) |
When it runs first time | Only on change (unless immediate: true) | Runs immediately once |
Access to old/new value | ✅ Yes (newVal, oldVal) | ❌ No (no diff info) |
Typical use | React to a specific piece of state | Run an effect that depends on many things |
Predictability | High | Lower (depends on what gets accessed) |
1. watch(): explicit, targeted, predictable
You tell Vue exactly what to watch. It runs only when that source changes.
import { ref, watch } from 'vue';
const query = ref('');
watch(query, (newVal, oldVal) => {
console.log('query changed from', oldVal, 'to', newVal);
// e.g. fetchData(newVal)
});
By default, this does not run on mount. You can opt in:
watch(query, () => { /* ... */ }, { immediate: true });
2. watchEffect(): automatic, eager, convenient
Vue runs the function immediately, tracks every reactive value you touch, and re-runs it when any of them changes.
import { ref, watchEffect } from 'vue';
const query = ref('');
const page = ref(1);
watchEffect(() => {
// Vue automatically tracks: query.value and page.value
fetchData(query.value, page.value);
});
3. When should you use which?
Situation | Prefer |
|---|---|
React to one specific value changing | watch() |
Need old vs new value | watch() |
Effect depends on many reactive values | watchEffect() |
You want it to run immediately | watchEffect() (or watch + immediate) |
You want maximum predictability | watch() |
4. A real recursion footgun with watch()
A watch() callback that mutates the source it watches can schedule itself again indefinitely. Because watch() tracks the explicit source separately from the callback, this write is treated as another source change.
import { ref, watch } from 'vue';
const count = ref(0);
watch(count, () => {
count.value++; // ❌ writes to the watched source again
});
count.value = 1; // starts the recursive updates
A direct synchronous watchEffect(() => count.value++) is different: Vue recognizes that the effect itself caused the change and suppresses that self-trigger, so the example runs once rather than looping forever. That protection is not a design recommendation—mutating dependencies inside an effect still creates surprising behavior when another writer changes the value, when effects form an indirect cycle, or when async work is involved.
watchEffect(() => {
count.value++; // runs once; the direct self-caused rerun is suppressed
});
// Prefer computed() for derived values and event handlers for explicit writes.
5. How to avoid recursion and hidden state flow
- Do not write back to a source from its own
watch()callback. - Prefer
computedfor derived state instead of synchronizing one ref into another. - Keep writes in explicit event or action paths, and use watcher cleanup for external side effects.
- Treat watchEffect self-trigger protection as a safety detail, not permission to mutate dependencies.
6. Subtle watchEffect trap: accidental dependencies
Because watchEffect tracks everything you read, even a console.log(someRef.value) or a debug read can become a dependency and retrigger the effect.
watchEffect(() => {
console.log(debugFlag.value); // now this is a dependency
fetchData(query.value);
});
7. Cleanup and async effects
Both APIs support cleanup via onCleanup, which is critical for cancelling requests or timers.
watchEffect((onCleanup) => {
const controller = new AbortController();
fetch(url.value, { signal: controller.signal });
onCleanup(() => controller.abort());
});
8. Practical rule of thumb
- If you can describe the dependency in one sentence: use watch().
- If the dependency is “whatever I touch in here”: use watchEffect().
- If you’re syncing state: ask yourself if this should be a computed instead.
Mental model: watch means “run when this explicit source changes”, while watchEffect means “rerun when a reactive value read synchronously here changes.” Automatic tracking is convenient, but it can also collect dependencies you did not intend.
Summary
Summary
watch()is explicit, lazy, and gives you old/new values.watchEffect()is automatic, eager, and tracks dependencies read synchronously.- A watch callback can recurse by writing to its watched source.
- Vue suppresses a direct synchronous watchEffect self-trigger, but effects should still not mutate their dependencies.
- Prefer
computedfor derived state and explicit actions for writes.
Use this as one explanation rep, then continue with the Vue.js interview questions cluster or a guided prep path.