Computed properties and watchers both react to reactive changes, but they solve different problems: computed is for derived state (cached, declarative), while watch is for side effects (async/imperative work triggered by changes).
Computed vs watch in Vue: derived state (cached) vs side effects (imperative)
Use guided tracks for structured prep, then practice company-specific question sets when you want targeted interview coverage.
Core idea
Computed = a derived value (like a formula). Vue tracks its dependencies and caches the result until one of them changes.
Watch = run code when something changes. It’s meant for side effects (fetching, logging, syncing, timers), not for producing values for the template.
Aspect | computed | watch / watchEffect |
|---|---|---|
Primary purpose | Derived state (calculate a value from other reactive state) | Side effects (do something when state changes) |
Caching | Yes (cached until deps change) | No (runs when triggered) |
Evaluation model | Lazy: recomputes when accessed after being invalidated | Eager: runs callback when source changes (timing configurable via flush) |
Return value | Yes (you read it like a value) | No (callback-driven; produces effects, not a value) |
Async work | Avoid (keep it pure) | Yes (common: API calls + cancellation) |
Common mistake | Putting side effects in computed | Using watch to keep derived state in sync (duplicate state) |
Anti-pattern (common in interviews): using watch for derived state
This duplicates state and can drift out of sync. Prefer computed unless you truly need an effect.
<!-- BAD: derived state via watch (duplicate state) -->
<script setup>
import { ref, watch, computed } from 'vue';
const first = ref('Mina');
const last = ref('Yilmaz');
const fullNameViaWatch = ref('');
watch([first, last], ([f, l]) => {
fullNameViaWatch.value = `${f} ${l}`;
}, { immediate: true });
// GOOD: derived state via computed (cached, always in sync)
const fullName = computed(() => `${first.value} ${last.value}`);
</script>
<template>
<p>watch-derived: {{ fullNameViaWatch }}</p>
<p>computed: {{ fullName }}</p>
</template>
Watchers (the right use): side effects + async + cancellation
When a value changes and you need to do something (fetch, analytics, sync URL, write to storage), use watch. Use cleanup to avoid race conditions (old request finishing after a new one).
<script setup>
import { ref, watch } from 'vue';
const query = ref('');
const results = ref([]);
const error = ref(null);
watch(query, async (q, _prev, onCleanup) => {
if (!q.trim()) {
results.value = [];
error.value = null;
return;
}
const ctrl = new AbortController();
onCleanup(() => ctrl.abort());
try {
error.value = null;
const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`, { signal: ctrl.signal });
results.value = await res.json();
} catch (e) {
// Ignore abort; handle real errors
if (e?.name !== 'AbortError') error.value = e;
}
}, { flush: 'post' });
</script>
<template>
<input v-model="query" placeholder="Search..." />
<pre v-if="error">{{ error }}</pre>
<ul>
<li v-for="r in results" :key="r.id">{{ r.title }}</li>
</ul>
</template>
Key options you should know (interview-level)
immediate: trueruns the watcher once on setup (useful for initial fetch).deep: truewatches nested mutations (use sparingly; can be expensive). Prefer watching a specific getter like() => obj.a.b.flush:'pre'(default),'post'(after DOM updates),'sync'(runs immediately; use carefully).watchEffect(): tracks dependencies automatically (great for effects that depend on many reactive reads), but use it only for effects (same rule: not for derived display values).
Rule of thumb
If the result is a value you want to render (filtering, formatting, totals) → computed.
If you need to do something when it changes (fetch, sync, log, imperative updates) → watch.