Watch option purpose and side effects
The watch option is used to trigger a function whenever a reactive property changes. It is designed to perform side effects in reaction to state changes, such as mutating the DOM or changing another piece of state based on async operation results. Unlike computed properties which declaratively compute derived values, watchers allow imperative operations.
Options API watch with dot-delimited path
In the Options API, the watch option supports dot-delimited paths as keys to watch nested properties. Only simple paths are supported; expressions are not supported. Example: watch: { 'some.nested.key'(newValue) { } }
Composition API watch function basic usage
The watch function in Composition API triggers a callback whenever a piece of reactive state changes. It accepts the reactive source as the first argument and a callback function as the second argument. The callback receives newValue and oldValue parameters. Example: watch(question, async (newQuestion, oldQuestion) => { if (newQuestion.includes('?')) { /* do something */ } })
Watch source types in Composition API
The first argument to watch() can be different types of reactive sources: a ref (including computed refs), a reactive object, a getter function, or an array of multiple sources. You cannot directly watch a property of a reactive object like watch(obj.count, callback); instead use a getter: watch(() => obj.count, callback).
Watch source types example code
const x = ref(0)
const y = ref(0)
// single ref
watch(x, (newX) => {
console.log(`x is ${newX}`)
})
// getter
watch(
() => x.value + y.value,
(sum) => {
console.log(`sum of x + y is: ${sum}`)
}
)
// array of multiple sources
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x is ${newX} and y is ${newY}`)
})
Options API deep watcher configuration
In the Options API, watch is shallow by default and only triggers when the watched property is assigned a new value, not on nested property changes. To trigger on all nested mutations, use a deep watcher by setting the deep option to true. Example: watch: { someObject: { handler(newValue, oldValue) { }, deep: true } }
Composition API deep watcher on reactive object
When you call watch() directly on a reactive object in Composition API, it implicitly creates a deep watcher and the callback triggers on all nested mutations. Note that newValue will equal oldValue because they point to the same object. If you use a getter that returns a reactive object, the callback only fires if the getter returns a different object, unless you explicitly set deep: true option.
Deep watcher depth control in Vue 3.5+
In Vue 3.5+, the deep option can be a number indicating the maximum traversal depth for how many levels Vue should traverse an object's nested properties.
Deep watcher performance warning
Deep watch requires traversing all nested properties in the watched object and can be expensive when used on large data structures. Use it only when necessary and be aware of the performance implications.
Options API eager watcher with immediate option
By default watch is lazy and the callback won't be called until the watched source changes. To force eager execution, use immediate: true option. The initial execution happens just before the created hook, and Vue will have already processed data, computed, and methods options. Example: watch: { question: { handler(newQuestion) { }, immediate: true } }
Composition API eager watcher with immediate option
Use the immediate: true option in watch() to force the callback to execute immediately, then again when the source changes. Example: watch(source, (newValue, oldValue) => { }, { immediate: true })
Once watcher option Vue 3.4+
In Vue 3.4+, use the once: true option to make a watcher callback execute only once when the source changes. Example Options API: watch: { source: { handler(newValue, oldValue) { }, once: true } }. Example Composition API: watch(source, (newValue, oldValue) => { }, { once: true })
watchEffect purpose and automatic dependency tracking
watchEffect() allows automatic tracking of a callback's reactive dependencies without explicitly specifying sources. The callback runs immediately and automatically tracks reactive dependencies accessed during synchronous execution. When tracked dependencies change, the callback runs again. This is useful when the same reactive state used inside the callback would otherwise need to be passed as the source.
watchEffect automatic dependency tracking example
const todoId = ref(1)
const data = ref(null)
watchEffect(async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
})
This automatically tracks todoId.value as a dependency without needing to explicitly pass it as the source.
watchEffect synchronous execution constraint
watchEffect only tracks dependencies during its synchronous execution. When using it with an async callback, only properties accessed before the first await tick will be tracked.
watch vs watchEffect dependency tracking difference
watch only tracks the explicitly watched source and won't track anything accessed inside the callback. The callback only triggers when the source has actually changed, separating dependency tracking from side effects and providing precise control. watchEffect combines dependency tracking and side effect into one phase, automatically tracking every reactive property accessed during synchronous execution, which is more convenient but makes reactive dependencies less explicit.
onWatcherCleanup API for cleanup functions
The onWatcherCleanup() API (available in Vue 3.5+) registers a cleanup function that will be called when the watcher is invalidated and is about to re-run. This is useful for canceling stale async requests. Must be called during the synchronous execution of a watchEffect effect function or watch callback function; cannot be called after an await statement.
onWatcherCleanup example with AbortController
import { watch, onWatcherCleanup } from 'vue'
watch(id, (newId) => {
const controller = new AbortController()
fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {
// callback logic
})
onWatcherCleanup(() => {
// abort stale request
controller.abort()
})
})
onCleanup callback function parameter
An onCleanup function can be passed to watcher callbacks as the 3rd argument (Options and Composition API), and to the watchEffect effect function as the first argument (Composition API only). Unlike onWatcherCleanup, onCleanup passed via function argument is bound to the watcher instance and is not subject to the synchronous constraint.
onCleanup example code
watch(id, (newId, oldId, onCleanup) => {
// ...
onCleanup(() => {
// cleanup logic
})
})
watchEffect((onCleanup) => {
// ...
onCleanup(() => {
// cleanup logic
})
})
Default watcher callback flush timing
By default, a watcher's callback is called after parent component updates (if any), and before the owner component's DOM updates. This means if you attempt to access the owner component's own DOM inside a watcher callback, the DOM will be in a pre-update state.
Post flush watcher to access updated DOM
To access the owner component's DOM in a watcher callback after Vue has updated it, specify the flush: 'post' option. Example Options API: watch: { key: { handler() {}, flush: 'post' } }. Example Composition API: watch(source, callback, { flush: 'post' })
watchPostEffect convenience alias
watchPostEffect() is a convenience alias for watchEffect with flush: 'post'. It executes after Vue updates. Example: import { watchPostEffect } from 'vue'; watchPostEffect(() => { /* executed after Vue updates */ })
Sync watcher with flush sync option
A watcher that fires synchronously, before any Vue-managed updates, can be created with flush: 'sync' option. Example Options API: watch: { key: { handler() {}, flush: 'sync' } }. Example Composition API: watch(source, callback, { flush: 'sync' })
watchSyncEffect convenience alias
watchSyncEffect() is a convenience alias for watchEffect with flush: 'sync'. It executes synchronously upon reactive data change. Example: import { watchSyncEffect } from 'vue'; watchSyncEffect(() => { /* executed synchronously upon reactive data change */ })
Sync watcher performance warning
Sync watchers do not have batching and trigger every time a reactive mutation is detected. It is ok to use them to watch simple boolean values, but avoid using them on data sources that might be synchronously mutated many times, such as arrays.
Options API $watch() instance method
The $watch() instance method can be used to imperatively create watchers. This is useful when you need to conditionally set up a watcher or only watch something in response to user interaction. Example: export default { created() { this.$watch('question', (newQuestion) => { }) } }
$watch() returns function to stop watcher
The $watch() API returns a function that can be called to stop the watcher. Example: const unwatch = this.$watch('foo', callback); unwatch();
Automatic watcher cleanup on component unmount Options API
Watchers declared using the watch option or the $watch() instance method are automatically stopped when the owner component is unmounted. In most cases you do not need to worry about stopping the watcher yourself.
Automatic watcher cleanup on component unmount Composition API
Watchers declared synchronously inside setup() or <script setup> are bound to the owner component instance and will be automatically stopped when the owner component is unmounted. In most cases, you do not need to worry about stopping the watcher yourself. The key is that the watcher must be created synchronously.
Manual stop of Composition API watcher
To manually stop a watcher in Composition API, use the returned handle function. This works for both watch and watchEffect. Example: const unwatch = watchEffect(() => {}); unwatch();
Async watchers must be stopped manually to avoid memory leaks
If a watcher is created in an async callback, it will not be bound to the owner component and must be stopped manually to avoid memory leaks. Synchronous creation should be preferred whenever possible. If you need to wait for async data, make your watch logic conditional instead.
Conditional watch logic instead of async watchers example
// data to be loaded asynchronously
const data = ref(null)
watchEffect(() => {
if (data.value) {
// do something when data is loaded
}
})
watchEffect() creates a reactive effect
watchEffect() is Vue's API that creates a reactive effect. It automatically tracks dependencies accessed during its callback execution and re-runs the callback whenever those dependencies change.
Watcher debugging with onTrack and onTrigger
watch() and watchEffect() accept options with onTrack and onTrigger callbacks. In the Options API, watchers declared with object syntax also support these callbacks. onTrack is called when a dependency is tracked, onTrigger when a dependency mutates. Both receive debugger events. These options only work in development mode.