We use the watch()
function to trigger a callback whenever a piece of reactive state changes. It is similar to computed properties, but in this case, a watcher allow side effects.
const value = ref('') const oldVal = ref('') const newVal = ref('') // Watching a single source watch(value, (newValue, oldValue) => { oldVal.value = oldValue newVal.value = newValue }) // We can also watch a getter watch( () => x.value + y.value, (sum) => { console.log(`sum of x + y is: ${sum}`) } ) // Watching multiple sources watch([x, () => y.value], ([newX, newY]) => { console.log(`x is ${newX} and y is ${newY}`) }) // If you want to watch a nested property, use a getter instead of the direct property value // For example, this won't work: // // watch(obj.count, (count) => { // console.log(`Count is: ${count}`) // }) // const obj = reactive({ count: 0 }) watch( () => obj.count, (count) => { console.log(`Count is: ${count}`) } )
<p>Old value: {{ oldVal }}</p> <p>New value: {{ newVal }}</p> <input type="text" v-model="value" />
With watch()
we need to explicitly determine which reactive state whould be watched. In some cases it is necessary to track the changes of different states, and execute only one callback. For this scenario we can use watchEffect()
.
const text = ref('') watchEffect(async () => { console.log(`The text has changed: ${text.value}`) }) // Data to be loaded asynchronously const data = ref(null) watchEffect(() => { if (data.value) { // do something when data is loaded } })
We can use some options to change the behavior of a watcher. We set some options like deep
, immediate
, once
, flush
, and methods like onTrack()
or onTrigger()
.
With deep
, we can define the changes detection in all nested properties of an object. Natively, watchers for reactive objects are deep by default. However, if we create a watcher for a nested property, changes will only be detected when the whole object is replaced, and not when its nested properties change. In order to make a deep detection we set the option deep
to true
.
const obj = reactive({ count: 0 }) watch(obj, (newValue, oldValue) => { // fires on nested property mutations // Note: `newValue` will be equal to `oldValue` here // because they both point to the same object! }) obj.count++ watch( () => state.someObject, (newValue, oldValue) => { // Note: `newValue` will be equal to `oldValue` here // *unless* state.someObject has been replaced }, { deep: true } )
Watchers are lazy by default. They run only when changes are detected. However, in some cases it is necessary to run the callback function for the first time when the component is mounted, this means, one initial execution without a change detection. To enable this behavior we set the option immediate
to true
.
watch( source, (newValue, oldValue) => { // executed immediately, then again when `source` changes }, { immediate: true } )
For some other scenarios, we would need only one single execution. For this purpose we enable the option once
watch( source, (newValue, oldValue) => { // when `source` changes, triggers only once }, { once: true } )
With flush
option, we can determine in which moment the watcher is executed. By default, they are executed before DOM updates in the component ('pre'
), but we can make the execution perform after DOM updates ('post'
). Sometimes it can be useful execute the watcher while the DOM is being updated ('sync'
), but this should only be used for simple states, because for complex ones the watcher can be executed with every state change while the DOM is changing.
import { watchPostEffect, watchSyncEffect } from 'vue' watch(source, callback, { flush: 'post' // 'pre' | 'post' | 'sync' }) watchEffect(callback, { flush: 'post' // 'pre' | 'post' | 'sync' }) // We can achieve the same with watchPostEffect and watchSyncEffect watchPostEffect(() => { /* executed after Vue updates */ }) watchSyncEffect(() => { /* executed synchronously upon reactive data change */ })
We can use onTrack()
and onTrigger()
for debugging purposes, and can be only used in dev mode.
watch(source, callback, { // triggered when count.value is tracked as a dependency onTrack(e) { debugger }, // triggered when count.value is mutated onTrigger(e) { debugger } }) watchEffect(callback, { onTrack(e) { debugger }, onTrigger(e) { debugger } })
We can use the onWatcherCleanup()
API to register a cleanup function that will be called when the watcher is invalidated and is about to re-run. Vue 3.5+.
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() }) })
Alternatively, an onCleanup
function is also passed to watcher callbacks as the 3rd argument, and to the
effect function as the first argument. Before Vue 3.5.watchEffect
watch(id, (newId, oldId, onCleanup) => { // ... onCleanup(() => { // cleanup logic }) }) watchEffect((onCleanup) => { // ... onCleanup(() => { // cleanup logic }) })
By default, watchers created synchronously in the component, are stopped once the component is unmounted. For thos that are not created like that, we can stop them by using the returned handle function from watch()
and watchEffect()
.
import { watchEffect } from 'vue' // this one will be automatically stopped watchEffect(() => {}) // ...this one will not! setTimeout(() => { watchEffect(() => {}) }, 100) const unwatch = watchEffect(() => {}) // ...later, when no longer needed unwatch()
References