Watchers
|
This section documents the current Vue 3.x release line as published at
the official Vue.js documentation, which is the reference these pages are written and
verified against. No specific patch version is pinned. The Composition API with This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, since Vue and its ecosystem iterate quickly. This section’s bibliography lists the reference material consulted while preparing these pages. |
A watcher runs a side effect when reactive state changes — fetching data, imperative DOM work, talking to
a non-reactive library, logging. For deriving a value from state, use a
computed property instead. The reference is
Watchers and the API entries for
watch and
watchEffect.
watch
watch(source, callback, options?). The source is a ref, a reactive object, a getter function, or an array
of those; the callback receives the new and previous values.
<script setup>
import { ref, watch } from 'vue'
const question = ref('')
const answer = ref('')
// source: a ref
watch(question, async (newQuestion, oldQuestion) => {
if (!newQuestion.includes('?')) return
answer.value = 'Thinking...'
const res = await fetch('https://yesno.wtf/api')
answer.value = (await res.json()).answer
})
</script>
A single property of a reactive object is not a valid source on its own (it evaluates to a plain value) — wrap it in a getter:
const state = reactive({ count: 0 })
// watch(state.count, ...) // wrong -- passes a number
watch(() => state.count, (count, prev) => {
console.log(count, prev)
})
Multiple sources — pass an array; the callback gets arrays of new and old values:
watch([fooRef, () => bar.value], ([foo, bar], [prevFoo, prevBar]) => {
/* ... */
})
deep
Passing a reactive object directly makes the watcher implicitly deep — it fires on nested mutation, though
newValue and oldValue are then the same object. A getter that returns an object is not deep unless you
ask; deep also accepts a number to cap the traversal depth (3.5+).
watch(
() => state.profile,
(profile) => { /* also fires when state.profile.name changes */ },
{ deep: true }
)
watchEffect
watchEffect(fn) runs fn immediately and re-runs it whenever any reactive value it read during that run
changes. Dependencies are tracked automatically — there is no source list. See
watchEffect.
const id = ref(1)
const data = ref(null)
watchEffect(async () => {
const res = await fetch(`/api/todos/${id.value}`)
data.value = await res.json()
})
Only synchronous reads before the first await are tracked. A ref accessed after an await does not
register as a dependency.
watch vs. watchEffect
watch |
watchEffect |
|---|---|
explicit source(s) |
tracks whatever it reads |
lazy by default ( |
runs once immediately |
gives |
no previous value |
callback is separate from the source |
tracking and effect in one function |
Prefer watch when you need the old value or want to be exact about the trigger; watchEffect when the
effect touches several sources you would otherwise have to list.
Flush timing
By default the callback runs before Vue patches the DOM (flush: 'pre'). The alternatives:
-
flush: 'post'— run after the DOM update, so template refs hold the updated elements.watchPostEffect()is the shorthand forwatchEffectwith this option. -
flush: 'sync'— run synchronously on every change, before batching. Inefficient; rarely the right call.
watch(source, callback, { flush: 'post' })
Cleanup
Register cleanup to run before the next callback (and when the watcher stops) — abort a stale request,
clear a timer. onWatcherCleanup() (3.5+) is importable and must be called synchronously inside the effect:
import { watch, onWatcherCleanup } from 'vue'
watch(id, (newId) => {
const controller = new AbortController()
fetch(`/api/${newId}`, { signal: controller.signal })
onWatcherCleanup(() => controller.abort())
})
The older form is an onCleanup function passed to the callback — the third argument for watch, the first
for watchEffect:
watch(id, (newId, oldId, onCleanup) => {
const controller = new AbortController()
onCleanup(() => controller.abort())
})
Stopping a watcher
watch and watchEffect return a stop handle:
const stop = watchEffect(() => { /* ... */ })
// later
stop()
A watcher created synchronously in setup / <script setup> is bound to the component and stops on
unmount. One created asynchronously — inside setTimeout, or after an await — is not bound and leaks
unless you stop it by hand. The docs state watchers must be created synchronously to be disposed
automatically.
Options API contrast
The Options API exposes watch: keyed by the watched property, with a method, a string method name, or an
options object as the value. See the watch option.
export default {
data() {
return { question: '', form: { email: '' } }
},
watch: {
question(newVal, oldVal) { /* ... */ },
form: {
handler(val) { /* ... */ },
deep: true,
immediate: true
},
'form.email'(val) { /* dotted path as a string key */ }
}
}
this.$watch() is the imperative equivalent and returns a stop function.
watch vs. computed
A computed property returns a cached value and should be a pure function of other state. A watcher performs an effect and returns nothing. If you catch yourself assigning one ref inside a watcher purely to mirror another ref, you want a computed.
See also
-
Computed Properties — the cached, declarative alternative for derived state.
-
Template Refs — why
flush: 'post'matters when a watcher reads a ref. -
Lifecycle Hooks — where synchronously created watchers are disposed.