React
watch and computed in Vue 3: when to use which
computed produces a value with no side effects, watch runs code when a source changes. The choice depends on the output: a piece of data or an effect.
Key points
- If the function returns a value it is a computed; if it runs an effect it is a watch.
- computed caches the result and recalculates only when a dependency changes.
- watch declares its sources, receives old and new value, and accepts a cleanup to abort requests.
Use computed when you need a value derived from other state, and watch when you need to run an effect: a request, a log entry, a save. If the function returns nothing, it is not a computed.
Computed: a derived value
import { computed, ref } from "vue";
const query = ref("");
const rows = ref([]);
const visible = computed(() =>
rows.value.filter((row) =>
row.name.toLowerCase().includes(query.value.toLowerCase()),
),
);
The filter runs once per change of query or rows, not on every render. computed caches the result until a dependency changes, which is the practical difference from a method called in the template.
Watch: an effect with a source
import { ref, watch } from "vue";
const id = ref("1");
const user = ref(null);
watch(
id,
async (value, _old, onCleanup) => {
const controller = new AbortController();
onCleanup(() => controller.abort());
const res = await fetch("/api/users/" + value, {
signal: controller.signal,
});
user.value = await res.json();
},
{ immediate: true },
);
Here the result is not a value to display but a request to fire. immediate runs at once, and onCleanup aborts the previous call when id changes or the component unmounts.
The choice in three lines
| Case | Winner | Why |
|---|---|---|
| Cart total | computed | A derived value, no effect |
| Autosave of the draft | watch | Writes outside state, so it needs an effect |
| Page title from the user name | watch | It touches document.title, not a value |
| Filtering a list | computed | Cached and readable in the template |
Common mistakes
- Assigning inside a
computed: the compiler warns and the cache becomes unpredictable. - Using
watchEffectfor a derived value: it collects dependencies implicitly and recalculates unchecked. - Watching a large object with
deep: true: the traversal costs more than it gives. - Skipping the cleanup: two requests in flight write state in the wrong order.
The limitation to plan for
Neither function solves the first load: an async computed returns a Promise, not the data. For a value that arrives over the network you need a ref plus a watch with cleanup, as in the example above.
Where to go next
In the catalogue: shadcn-vue, PrimeVue and Vuetify; filtered tables are in components. The guides: Composition API and Pinia. The comparisons: derived state in React and Vue vs React.
Frequently asked questions
Can I use computed inside watch?
Yes, and it often helps: watching a computed reacts to the derived value instead of every field that builds it.
watch or watchEffect?
watch declares its sources and receives old and new value; watchEffect collects dependencies by itself and suits short effects.
Can computed be async?
No. An async function inside computed returns a Promise and the value is never awaited, so an async datum needs watch plus a ref.
Why does my watch never run?
The source is not reactive. A getter over a plain variable notifies nothing, so pass a ref or a function that reads one.
Should I use watch with deep?
Only for objects you change in depth. On large structures the traversal costs more than it gives, and watching one field is cheaper.