Skip to the solution
MediumFrontend10 min read

What is the difference between computed and watch?

594 views

The problem

Question presented to candidate: "Both computed and watch react to state changes in Vue. How do you decide which one to use? Give me an example where picking the wrong one causes a real bug."

What a strong answer should cover:

  • computed derives a value: it returns a read-only ref, is lazy (runs only when read), cached (re-runs only when a dependency changed) and should be a pure function.
  • watch performs a side effect when a source changes: fetching, logging, writing to storage, imperatively touching the DOM. It receives the new and old value and is lazy unless immediate: true.
  • A computed only notifies its dependents when its result changes (since Vue 3.4).
  • Anti-patterns: a watch that copies a derived value into another ref (stale until the next flush, extra state), a computed with side effects, and an async computed (its value is a Promise).
  • Watch callbacks are scheduled (flush pre by default); a computed is evaluated synchronously on read.
  • Options API equivalents are the computed and watch options; the reasoning is identical.

Clarifying questions expected:

  • "Is the value needed in the template, or is the goal to trigger something outside Vue?"
  • "Is any of the work asynchronous?"

Code / implementation expected: Yes — a filtered list with computed plus a watch that records each search.

  • computed
  • watch

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

10 min read · 10 sections

Read the code

VueFilter with computed, record searches with watch
<script setup>
import { ref, computed, watch } from "vue";

const products = ref(["Vue mug", "Vue shirt", "React mug", "Pinia sticker"]);
const query = ref("");
const history = ref([]);
let filterRuns = 0;

// computed: DERIVE a value. Cached, lazy, and re-run only when query or products change.
const visible = computed(() => {
  filterRuns++;
  const q = query.value.toLowerCase();
  return products.value.filter((p) => p.toLowerCase().includes(q));
});

// watch: DO something when a source changes. Here: a side effect (record the search).
watch(query, (now, before) => {
  history.value.push(`${before || "(empty)"} -> ${now}`);
  console.log(`watch fired: "${before}" -> "${now}"`);
});

function readTwice() {
  // Reading a cached computed again does not re-run the filter.
  console.log("count:", visible.value.length, visible.value.length, "| filter runs so far:", filterRuns);
}
</script>

<template>
  <input v-model="query" placeholder="search" />
  <ul>
    <li v-for="p in visible" :key="p">{{ p }}</li>
  </ul>
  <button @click="readTwice">Read twice</button>
  <p id="history">history: {{ history.join(" | ") }}</p>
</template>
Lazy, cached, change-aware computed vs watch, probed in Node
import { ref, computed, watch, effect, nextTick } from "vue";

const items = ref([3, 4, 5]);
let calls = 0;
const sum = computed(() => { calls++; return items.value.reduce((a, b) => a + b, 0); });

console.log("1. getter calls before any read:", calls);           // lazy
console.log("   read three times:", sum.value, sum.value, sum.value, "| getter calls:", calls); // cached
items.value.push(1);
console.log("   after push, before read, calls:", calls);          // still lazy: marked dirty only
console.log("   read again:", sum.value, "| calls:", calls);

// 2. A computed only notifies dependents when its VALUE changes
const n = ref(2);
const isEven = computed(() => n.value % 2 === 0);
let effectRuns = 0;
effect(() => { isEven.value; effectRuns++; });
n.value = 4; n.value = 6; n.value = 7;
console.log("2. effect runs (1 initial + changes of isEven only):", effectRuns);

// 3. watch: lazy by default, gives new and old values, meant for side effects
const query = ref("vue");
watch(query, (now, before) => console.log(`3. watch: "${before}" -> "${now}" (would fetch here)`));
query.value = "vue 3.5";
await nextTick();

// 4. async work inside computed gives you a Promise, not a value
const bad = computed(async () => query.value.toUpperCase());
console.log("4. async computed value:", bad.value instanceof Promise ? "a Promise" : bad.value);

// 5. writing to a getter-only computed
sum.value = 100;
console.log("5. sum after write attempt:", sum.value);

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

What is keep-alive in Vue?

Continue