watch vs watchEffect in Vue: what’s the difference, when does each run, and how can you accidentally create infinite loops?

LowHardVue
Preparing for interviews?

Use guided tracks for structured prep, then practice company-specific question sets when you want targeted interview coverage.

Quick Answer

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.

Answer

Overview

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

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

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

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

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

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 computed for derived state, and use guards when mutating inside watchers.

Similar questions
Guides
23 / 34