Computed properties derive values; watchers perform side effects when state changes: fetching data, writing to localStorage, logging, or touching the DOM. Vue offers two tools, watch and watchEffect, which differ in how they know what to track and when they run. After this lesson you will pick the right one, control timing with options, and cancel stale work so fast-changing state never produces out-of-order results.
watch takes a source and a callback. The callback runs only when the source changes and receives the new and old values:
import { ref, reactive, watch } from 'vue'
const query = ref('')
const filters = reactive({ category: 'all', minPrice: 0 })
watch(query, (value, oldValue) => {
console.log(`query: "${oldValue}" -> "${value}"`)
})
// A getter for a single property of a reactive object
watch(() => filters.category, (category) => loadProducts(category))
// Several sources at once
watch([query, () => filters.minPrice], ([q, price]) => search(q, price))Valid sources are a ref, a getter function, a reactive object, or an array of those. Watching a reactive object directly creates a deep watcher automatically. Passing filters.category (a plain string) as a source does not work; wrap it in a getter.
watchEffect runs its function immediately, records every reactive value read during that run, and re-runs whenever any of them changes:
import { ref, watchEffect } from 'vue'
const page = ref(1)
const pageSize = ref(20)
const items = ref([])
watchEffect(async () => {
const res = await fetch(`/api/items?page=${page.value}&size=${pageSize.value}`)
items.value = await res.json()
})There is no source list to maintain, which is convenient when an effect depends on several values. The trade-off: only values read synchronously before the first await are tracked, and you do not get the previous value.
| | watch | watchEffect |
| --- | --- | --- |
| Runs on creation | No (unless immediate: true) | Yes |
| Dependencies | Declared explicitly | Collected automatically |
| Old value available | Yes | No |
| Best for | Reacting to one specific change | Effects that read several values |
watch(source, callback, {
immediate: true, // run once right away with the current value
deep: true, // track nested mutations of objects/arrays
once: true, // stop after the first trigger (Vue 3.4+)
flush: 'post' // run after the DOM has updated (default is 'pre')
})deep: true is needed when watching a ref that holds an object and you mutate nested properties instead of replacing the object. It is expensive on large structures, so prefer a getter for the exact property when possible; since Vue 3.5 deep also accepts a number to limit traversal depth.
flush: 'post' matters when the callback must read the updated DOM, for example to measure an element after a list re-rendered (watchPostEffect is the watchEffect equivalent). flush: 'sync' runs on every change synchronously and should be rare.
When a watcher re-runs before the previous asynchronous work finished, the old result can arrive last and overwrite the new one. Register a cleanup that cancels the previous run:
import { ref, watch, onWatcherCleanup } from 'vue'
const userId = ref(1)
const user = ref(null)
watch(userId, async (id) => {
const controller = new AbortController()
onWatcherCleanup(() => controller.abort())
try {
const res = await fetch(`/api/users/${id}`, { signal: controller.signal })
user.value = await res.json()
} catch (err) {
if (err.name !== 'AbortError') throw err
}
})onWatcherCleanup (Vue 3.5) must be called synchronously before the first await. In earlier versions the callback receives an onCleanup function as its third argument (first argument in watchEffect). The cleanup also runs when the watcher stops, so it doubles as teardown for timers and subscriptions.
Watchers created synchronously inside <script setup> stop automatically when the component unmounts. To stop one earlier, use the returned handle: const handle = watchEffect(...), then handle.stop(); since Vue 3.5 the handle also has pause() and resume(). Watchers created inside an async callback are not tied to the component, so create them synchronously or stop them yourself.
computed property.deep: true when watching ref([]) and pushing into the array.Which values does `watchEffect` track?
watch needs an explicit source (ref, getter, reactive object or array) and gives old and new values.watchEffect runs immediately and tracks whatever it reads synchronously.immediate, deep, once and flush: 'post' control when and how a watcher fires.onWatcherCleanup (or the onCleanup argument) to abort stale async work.stop, pause and resume.Next lesson: Lifecycle Hooks — run code when a component is mounted, updated and unmounted.