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?

HighIntermediateVue

Interview quick answer

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.

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
Practice more Vue.js interview questions
Interview answer drill

Use this Vue interview question to rehearse a quick answer, common mistake, follow-up, and production pitfall.

Full interview answer

Overview

watch() 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)

watch() vs watchEffect() at a glance

1. watch(): explicit, targeted, predictable

You tell Vue exactly what to watch. It runs only when that source changes.

JAVASCRIPT
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:

JAVASCRIPT
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.

JAVASCRIPT
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()

Practical decision table

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.

JAVASCRIPT
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.

JAVASCRIPT
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 computed for 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.

JAVASCRIPT
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.

JAVASCRIPT
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 computed for derived state and explicit actions for writes.

Guides
Preparing for interviews?