Skip to the solution
MediumFrontend11 min read

What is the difference between watch and watchEffect?

1.1k views

The problem

Question presented to candidate: "Vue 3 gives you both watch and watchEffect. What is the actual difference between them, and how do you choose? Talk me through a case where watchEffect surprises people."

What a strong answer should cover:

  • watch(source, cb) tracks only the declared source (a ref, a getter, a reactive object or an array of these) and calls cb(newValue, oldValue).
  • watchEffect(fn) has no source list: it runs fn immediately and tracks every reactive read made during that run, re-collecting dependencies on each run.
  • watch is lazy by default (immediate: true changes that); watchEffect is always eager. Only watch gives you the old value.
  • With watchEffect, only reads before the first await are tracked, and conditional branches only track what actually ran.
  • Both return a handle to stop them (plus pause/resume since 3.5) and both are stopped automatically with the component.
  • Choose watch when you need precise control or the old value; watchEffect when the effect reads several values and should simply re-run when any of them change.

Clarifying questions expected:

  • "Does the side effect need the previous value, or just the current state?"
  • "Is the work async? That changes what watchEffect can track."

Code / implementation expected: Yes β€” the same pair of refs observed with watch and with watchEffect, logging when each runs.

  • watch
  • watcheffect

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

11 min read Β· 11 sections

Read the code

Vuewatch, watch with immediate, and watchEffect logging side by side
<script setup>
import { ref, watch, watchEffect } from "vue";

const name = ref("Ada");
const city = ref("London");
const log = ref([]);
const note = (line) => { log.value.push(line); console.log(line); };

// watch: explicit source, LAZY (no run at setup), gets new AND old value.
watch(name, (now, before) => note(`watch: name ${before} -> ${now}`));

// watchEffect: no source list, runs IMMEDIATELY, tracks every ref it reads.
watchEffect(() => note(`watchEffect: ${name.value} in ${city.value}`));

// watch can be made eager too, with { immediate: true }.
watch(city, (now, before) => note(`watch(immediate): city ${before} -> ${now}`), { immediate: true });
</script>

<template>
  <input id="name" v-model="name" />
  <input id="city" v-model="city" />
  <ol>
    <li v-for="(line, i) in log" :key="i">{{ line }}</li>
  </ol>
</template>
Explicit vs automatic tracking, async tracking and invalid sources (Node)
import { ref, reactive, watch, watchEffect, nextTick } from "vue";
const tick = () => nextTick();

const userId = ref(1);
const lang = ref("en");
const showDebug = ref(false);
const debugLevel = ref(1);

console.log("--- setup");
watch(userId, (id, old) => console.log(`watch(userId): ${old} -> ${id}`));
watchEffect(() => {
  // runs NOW, and tracks whatever it happens to read
  let line = `watchEffect: user ${userId.value}, lang ${lang.value}`;
  if (showDebug.value) line += `, debug ${debugLevel.value}`;
  console.log(line);
});
await tick();

console.log("--- lang = de (only watchEffect reads lang)");
lang.value = "de"; await tick();
console.log("--- debugLevel = 2 (not read yet: nothing runs)");
debugLevel.value = 2; await tick();
console.log("--- showDebug = true");
showDebug.value = true; await tick();
console.log("--- debugLevel = 3 (now it is a dependency)");
debugLevel.value = 3; await tick();
console.log("--- userId = 2 (both)");
userId.value = 2; await tick();

console.log("--- async: only reads before the first await are tracked");
const page = ref(1);
const size = ref(10);
watchEffect(async () => {
  const p = page.value;                 // tracked
  await Promise.resolve();
  console.log(`async effect: page ${p}, size ${size.value}`); // size read after await: NOT tracked
});
await tick(); await tick();
size.value = 50; await tick(); await tick();
console.log("(size change did not re-run it)");
page.value = 2; await tick(); await tick();

console.log("--- watch sources");
const state = reactive({ count: 0, nested: { a: 1 } });
watch(() => state.count, (v) => console.log("getter source fired:", v));
watch([userId, () => state.count], ([id, c], [oldId, oldC]) => console.log(`array source: [${oldId},${oldC}] -> [${id},${c}]`));
watch(state.count, () => {});
state.count++; await tick();

console.log("--- stop handles");
const stop = watchEffect(() => console.log("stoppable sees", userId.value));
stop();
userId.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 20 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

How do <Transition>, <KeepAlive> and <Suspense> compose in Vue 3?

Continue