watch and watchEffect

watch names its source and watchEffect finds its own: deep, immediate, once, cleanup, and stopping a watcher.

Syntaxwatch(source, (value, oldValue, onCleanup) => {}, { immediate, deep, once, flush })

watch and watchEffect run code in response to a change. That is the difference from computed, which produces a value. If what you want is a value, use a computed. If what you want is an action — write to localStorage, start a timer, set the document title — use a watcher.

The two forms

watch(source, callback, options) states its source. A source is a ref, a getter function, a reactive object, or an array of those. The callback receives (value, oldValue, onCleanup) and does not run until the source changes.

watchEffect(effect) takes no source. It runs the function immediately, tracks whatever reactive state the function read, and re-runs when any of it changes. Shorter, and easy to over-subscribe: every ref you touch becomes a trigger.

Options

immediate: true fires once at creation, with oldValue undefined. deep: true traverses nested properties, and from Vue 3.5 it can be a number capping the depth. once: true (3.4) stops the watcher after the first call. flush: 'post' delays the callback until after Vue has updated the DOM.

Stopping

Both return a WatchHandle: call it, or use .stop(), plus .pause() and .resume() from 3.5. A watcher created synchronously in <script setup> stops itself when the component unmounts. One created in a callback or after an await does not, and leaks until you stop it.

Example

Example · vue
<!-- src/components/SearchBox.vue -->
<script setup lang="ts">
import { ref, watch, watchEffect } from 'vue'

const query = ref(localStorage.getItem('query') ?? '')
const saves = ref(0)

// Explicit source: runs only when query changes.
watch(query, (value: string, previous: string) => {
  localStorage.setItem('query', value)
  if (previous !== '') {
    saves.value += 1
  }
})

// No source: runs now, and again whenever query changes.
watchEffect(() => {
  document.title = query.value === '' ? 'Search' : `Search: ${query.value}`
})

function onInput(event: Event): void {
  query.value = (event.target as HTMLInputElement).value
}
</script>

<template>
  <input type="search" :value="query" @input="onInput" />
  <p>{{ saves }} edits saved</p>
</template>

<style scoped>
input {
  font: inherit;
}
</style>

When to use it

  • Persist a value to localStorage every time it changes.
  • Start and tear down a timer or event listener when a setting changes.
  • Set the document title or focus an element in reaction to state, not as a derived value.

More examples

Watching one property of a reactive object

The getter form is how you watch anything that is not itself a ref. Note the timing: with the default flush of 'pre' the callback is queued, so the log appears on the next tick rather than on the assignment line. Passing the whole object instead, watch(state, cb), works too but is implicitly deep and fires on any field.

Example · ts
// src/lib/pager.ts
import { reactive, watch } from 'vue'

const state = reactive({ page: 1, size: 20 })

// Wrong: state.page is a plain number by the time watch receives it.
// watch(state.page, () => {})

// Right: a getter, which watch can re-read.
watch(
  () => state.page,
  (page: number, previous: number) => {
    console.log(`page ${previous} -> ${page}`)
  }
)

state.page = 2
// Output: page 1 -> 2

Cleaning up before the next run (Vue 3.5+)

Any watcher that starts something must stop it, or a second change leaves two timers running. onWatcherCleanup was added in 3.5 and must be called synchronously inside the callback, never after an await. The older equivalent is the third callback parameter, onCleanup, which does the same job and works on every Vue 3 version.

Example · ts
// src/lib/countdown.ts
import { onWatcherCleanup, ref, watch } from 'vue'

const seconds = ref(5)
const left = ref(0)

watch(
  seconds,
  (value: number) => {
    left.value = value
    const timer = window.setInterval(() => {
      left.value -= 1
    }, 1000)

    // Runs before the next call and when the watcher stops.
    onWatcherCleanup(() => {
      window.clearInterval(timer)
    })
  },
  { immediate: true }
)

Stopping, pausing and running once

The return value is a WatchHandle: callable, and carrying stop, pause and resume, the last two added in 3.5. Use it for a watcher that should outlive nothing in particular, or that you create outside setup, for instance inside a setTimeout, where Vue cannot stop it for you on unmount.

Example · ts
// src/lib/handles.ts
import { ref, watch } from 'vue'

const ready = ref(false)

// once: true (Vue 3.4+) stops the watcher after one call.
watch(
  ready,
  (value: boolean) => {
    console.log(`ready: ${value}`)
  },
  { once: true }
)

const handle = watch(ready, () => {})
handle.pause()  // 3.5: hold the callback
handle.resume() // 3.5: let it run again
handle.stop()   // identical to calling handle()
// Output: ready: true  (after ready.value = true)

Discussion

  • Be the first to comment on this lesson.