Watchers and side effects

Vue · Practical · 12 min

Fetching on input changes, syncing to localStorage and talking to non-Vue code all need watchers.

Computeds derive values; watchers run side effects in response to changes.

`watch(source, callback, options)` watches an explicit source: a ref, a reactive object, a getter function, or an array of these. The callback receives (newValue, oldValue, onCleanup).

  • To watch a property of a reactive object or a destructured prop, pass a getter: watch(() => props.id, ...) or watch(() => id, ...). Passing state.count directly passes a plain number and does not work.
  • { immediate: true } runs the callback once right away; { once: true } runs it only on the first change.
  • Watching a reactive object is deep automatically; for a ref holding an object, add { deep: true } (or a number to limit depth).

`watchEffect(fn)` runs immediately and re-runs whenever any reactive value it read changes, with no explicit source list. It is concise, but its dependencies are implicit and only those read synchronously are tracked.

Async watchers must handle stale results: if the id changes twice quickly, the first request may finish last. Vue 3.5 adds onWatcherCleanup(fn), called before the next run and on stop; use it to abort the old request.

By default callbacks run before the component updates; use { flush: 'post' } (or watchPostEffect) to see the updated DOM. Watchers created synchronously in setup stop with the component; both functions return a stop handle for manual control.

Example

import { ref, watch, watchEffect, onWatcherCleanup } from 'vue'

const userId = ref(1)
const user = ref(null)

watch(userId, async (id, oldId) => {
  const controller = new AbortController()
  onWatcherCleanup(() => controller.abort())   // runs if id changes again
  try {
    const res = await fetch(`/api/users/${id}`, { signal: controller.signal })
    user.value = await res.json()
  } catch (e) {
    if (e.name !== 'AbortError') throw e
  }
}, { immediate: true })

const theme = ref(localStorage.getItem('theme') ?? 'light')
watchEffect(() => {
  localStorage.setItem('theme', theme.value)   // re-runs when theme changes
  document.documentElement.dataset.theme = theme.value
})

Remember

  • watch(source, cb) for explicit sources; use a getter for reactive properties and props
  • watchEffect tracks whatever it reads synchronously and runs immediately
  • Abort stale async work with onWatcherCleanup (3.5+) or the onCleanup argument

Watch out for

  • watch(state.count, ...) passes a number, not a source; use watch(() => state.count, ...).
  • Using a watcher to copy derived data into another ref instead of a computed.

Check yourself

  1. Which correctly watches the `id` prop destructured with `const { id } = defineProps(...)`?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: vuejs.org/guide/essentials/watchers.html