Explain the difference between watch() and watchEffect() in Vue 3, when each one runs, how dependency tracking works (explicit vs automatic), and the common patterns that accidentally cause infinite update loops.
watch vs watchEffect in Vue: what’s the difference, when does each run, and how can you accidentally create infinite loops?
Use guided tracks for structured prep, then practice company-specific question sets when you want targeted interview coverage.
Overviewwatch() and watchEffect() both run side effects in response to reactive changes, but they work very differently: watch is explicit and lazy (you choose what to watch), while watchEffect is automatic and eager (Vue tracks everything you touch inside). Most bugs come from not realizing what is being tracked and from mutating the same state you’re watching.
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. The #1 footgun: creating an infinite loop
If your watcher mutates the same reactive state that it depends on, you can easily create a self-triggering loop.
import { ref, watchEffect } from 'vue';
const count = ref(0);
// ❌ Infinite loop
watchEffect(() => {
if (count.value < 10) {
count.value++; // This mutation retriggers the effect
}
});
The same bug can happen with watch() if you write back to the watched source without guarding:
watch(count, (v) => {
// ❌ This will loop forever
count.value = v + 1;
});
5. How to avoid infinite loops
- Don’t mutate the same source you are watching.
- If you must, add guards (if conditions, equality checks).
- Prefer deriving state with
computedinstead of syncing state with watchers.
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 is declarative (“watch this”), watchEffect is reactive magic (“rerun when anything I use changes”). Magic is convenient — and that’s why it’s easier to shoot yourself in the foot.
Summary
watch()is explicit, lazy, and gives you old/new values.watchEffect()is automatic, eager, and tracks dependencies implicitly.- Infinite loops happen when the effect mutates the same state it depends on.
- Prefer
computedfor derived state, and use guards when mutating inside watchers.