As apps grow, you constantly need values derived from other state -- a filtered list, a formatted price, a validity flag. You also sometimes need to run side effects when data changes -- saving to localStorage, fetching from an API. Vue gives you two dedicated tools: computed properties for derived state and watchers for side effects. Knowing which to reach for is a hallmark of clean Vue code.
A computed property is a reactive value calculated from other reactive values. It updates automatically and is cached: it only recalculates when a dependency changes.
<script setup>
import { ref, computed } from 'vue'
const items = ref([
{ name: 'Keyboard', price: 45, qty: 1 },
{ name: 'Mouse', price: 25, qty: 2 }
])
const total = computed(() =>
items.value.reduce((sum, i) => sum + i.price * i.qty, 0)
)
const formattedTotal = computed(() => '$' + total.value.toFixed(2))
</script>
<template>
<p>Order total: {{ formattedTotal }}</p>
</template>Change any price or quantity and total recomputes; render the page a hundred times without changes and the function runs zero extra times. Contrast that with calling a method in the template, which re-runs on every render.
items.value inside one.formattedTotal depending on total is idiomatic.Occasionally you want two-way derived state, such as binding v-model to a full name:
const first = ref('Ada')
const last = ref('Lovelace')
const fullName = computed({
get: () => first.value + ' ' + last.value,
set: (val) => { [first.value, last.value] = val.split(' ') }
})A watcher runs a callback whenever a source changes -- perfect for effects that live outside the rendering world:
import { ref, watch } from 'vue'
const query = ref('')
watch(query, async (newVal, oldVal) => {
if (newVal.length < 3) return
const res = await fetch('/api/search?q=' + encodeURIComponent(newVal))
console.log(await res.json())
})watch gives you both the new and old values, supports async callbacks, and can watch multiple sources at once: watch([a, b], ([newA, newB]) => { ... }).
watch(user, onChange, { deep: true }) // react to nested mutations
watch(theme, apply, { immediate: true }) // also run once right awaydeep: true is needed when watching a reactive object replaced property-by-property (watching a ref of an object whose .value is swapped does not need it).immediate: true fires the callback at setup time with the current value.watchEffect runs immediately and automatically tracks whatever reactive values it reads -- no explicit source list:
import { watchEffect } from 'vue'
watchEffect(() => {
document.title = query.value ? 'Searching: ' + query.value : 'My App'
})Use watchEffect for simple "keep X in sync with Y" effects; use watch when you need old values, lazy execution, or precise control over what triggers it.
Ask: am I producing a value, or performing an action?
| Need | Use |
|---|---|
| A filtered/sorted list, a formatted string, a boolean flag | computed |
| Save to localStorage, call an API, start a timer, log analytics | watch / watchEffect |
A common beginner mistake is using a watcher to copy data into another ref (watch(first, () => full.value = first.value + last.value)). That is derived state -- a computed does it in one line, cached, with no ordering bugs.
computed creates cached, reactive derived values; keep them pure.watch runs side effects with access to old and new values; add deep or immediate as needed.watchEffect auto-tracks dependencies and runs immediately.Next up: Components and Props -- splitting your UI into reusable, composable pieces.