Both computed() and watch() react when your state changes, so beginners often mix them up. Knowing their exact roles keeps your Vue 3 code predictable and free of redundant state.
Use computed() whenever you calculate a new value from existing reactive data. Use watch() or watchEffect() whenever a state change must trigger an external action such as an API call, timer, or DOM update.
flowchart TD
Goal{"What Do You Need?"}
Goal -->|Derive a New Value| Comp["Use computed() -> Returns Cached Ref"]
Goal -->|Run Async or Side Effect| Watch["Use watch() or watchEffect()"]
| Criteria | computed() |
watch() |
|---|---|---|
| Main Job | Calculate derived display state | Trigger side effects on change |
| Return Value | Read-only reactive ref | Returns a stop handle function |
| Caching | Cached until dependencies change | Runs callback on every matched change |
| Async Code | Synchronous only | Supports async/await and timers |
In real components, you often combine computed() for display strings with watch() for asynchronous network requests. Notice how fullName stays pure inside computed(), while query triggers an async fetch inside watch().
<script setup>
import { ref, computed, watch } from 'vue';
const firstName = ref('Jane');
const lastName = ref('Doe');
const query = ref('');
const apiResults = ref([]);
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`.trim();
});
watch(query, async (newQuery, _oldQuery, onCleanup) => {
const term = newQuery.trim();
if (term.length < 2) {
apiResults.value = [];
return;
}
const controller = new AbortController();
onCleanup(() => controller.abort());
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(term)}`, {
signal: controller.signal
});
apiResults.value = await res.json();
} catch (err) {
if (err.name !== 'AbortError') console.error(err);
}
});
</script>
<template>
<div class="reactive-demo">
<h3>User: {{ fullName }}</h3>
<input v-model="query" placeholder="Type at least 2 characters..." />
<ul>
<li v-for="item in apiResults" :key="item.id">{{ item.name }}</li>
</ul>
</div>
</template>
watch(): Writing watch(firstName, () => { fullName.value = ... }) adds extra refs and manual syncing. Replace that pattern with a single computed() property.async Requests Inside computed(): computed() expects a synchronous return value and ignores resolved promises for dependency tracking. Always run fetch() inside watch() or event handlers.AbortController in onCleanup, a slower earlier request can overwrite newer search results.watch() or watchEffect() for API fetching?Use watch() when you want strict control over which single ref triggers the fetch and want to skip running on mount. Use watchEffect() when you want it to run immediately and track multiple refs automatically.
Yes. Because computed() returns a reactive ref, you can pass it directly as the first argument to watch(fullName, (next) => ...).
Both watch() and watchEffect() return a stop function: const stop = watch(...). Call stop() whenever you want to tear down the watcher early.
Sign in to track your learning journey, earn industry-recognized certificates, and join our elite developer community.
Quick Access With