Notes on Vue watchers

by Odin Ravensson
on

development

website

note

vue

Vue watchers are essential for handing side effects on the state of data in Vue components.

// Reactive objects, direct or computed references
watch(foo, (newFoo, oldFoo) => {});

// Getters
watch(() => foo.bar, (newBar, oldBar) => {});

// Will not correctly watch a property of a reactive object
watch(foo.bar, (nb, ob) => console.info('Will never fire'));

Watchers automatically track the entire tree of a reäctive object. However, when using a getter to a property, the watcher will only fire if the property changes, meaning updates to the property’s own properties will not fire by default.

watch(() => foo.bar, () => console.info('Only fires when `bar` is replaced'));

watch(() => foo.doop, () => console.info('Now will fire if one of `doop`\'s properties is updated'), { deep: true });

As of 3.5, deep’s value may be an integer indicating how many levels down the watched property tree to track. Beware of performance issues when watching large or deep values. Consider using deep: false or deep: 1.

Watchers will—by default—only update when the watched value updates. One can force an immediate response by using immediate: true in the third parameter, the WatchOptions.

By using the option once: true, the callback will fire the first time the value updates and no other.

Nota Bene: the WatchOptions also has optional properties onTrack(event: DebuggerEvent) and onTrigger(event: DebuggerEvent) which are only used in development mode. onTrack will fire when Vue begins watching the value, and onTrigger will run each time the value is updated.[^1]

watchEffect() is similar to watch() except that the callback is the first parameter, and the watched input is omitted. It will automatically track any of the reactive values in the callback method—or at least until the first await[^2].

Watchers have three flush timings available in the WatchOptions; in run order they are 'sync', 'pre' and 'post'. 'pre' is the default and in this timing, Vue will combine all the changes of the value in the tick and will execute a single callback before updates are made to the DOM. Because it happens before the DOM updates, one will not have access to the owner component’s DOM. 'post' happens after the DOM updates, allowing access to the owner’s DOM. 'sync' runs whenever the value updates; this can cause significant performance issues. watchEffect has a convenience synchronous version: watchSyncEffect.

Because asynchronous execution may need to be canceled, there are hooks for that, either through the imported onWatchCleanup() method or the cleanup function passed as the third parameter in the callback function. Either of these take a closure and must be called in the synchronous portion of the callback. Use them to signal requests to stop or to exist early from synchronous functions. It works very well with an AbortController.

Watchers are automatically cleaned up if they are created synchronously with the component, but if they are created out of sync, they will need to be cleaned up manually to avoid a memory leak. This is done by calling the .stop() method on the object returned by watch() and related methods. This object also has .pause() and .resume(). These might be useful for reducing computation needs.

[^1] computed() has the same options too.

[^2] I assume anyway. Judging by Vue’s other call-outs of asynchronous code losing the context after the first await, I’d imagine this to be the case. It might merit a probe.