Skip to content

Watchers

You've already seen how computed properties let you derive values based on reactive state. However, computed getters are pure and should not produce "side effects". But sometimes, you need to run code in response to state changes, like fetching data or updating the DOM.

That's where the watch function comes in. It allows you to react to changes in reactive state by running a callback whenever the state updates.

vue
<script setup>
import { ref, watch } from 'vue'

const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')
const loading = ref(false)

// watch works directly on a ref
watch(question, async (newQuestion, oldQuestion) => {
  if (newQuestion.includes('?')) {
    loading.value = true
    answer.value = 'Thinking...'
    try {
      const res = await fetch('https://yesno.wtf/api')
      answer.value = (await res.json()).answer
    } catch (error) {
      answer.value = 'Error! Could not reach the API. ' + error
    } finally {
      loading.value = false
    }
  }
})
</script>

<template>
  <p>
    Ask a yes/no question:
    <input v-model="question" :disabled="loading" />
  </p>
  <p>{{ answer }}</p>
</template>

The first argument of watch can be several types of reactive "sources":

  • A ref (including computed refs)
  • A reactive object
  • A getter function
  • An array of multiple sources
javascript
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}`)
})

You cannot pass a property of a reactive object directly to watch:

javascript
const obj = reactive({ count: 0 })

// this won't work because we are passing a number to watch()
watch(obj.count, (count) => {
  console.log(`Count is: ${count}`)
})

Instead, wrap the property in a getter function:

javascript
// instead, use a getter:
watch(
  () => obj.count,
  (count) => {
    console.log(`Count is: ${count}`)
  }
)

Eager Watchers

watch is lazy by default: the callback won't be called until the watched source has changed. But in some cases we may want the same callback logic to be run eagerly (or immediately) - for example, we may want to fetch some initial data, and then re-fetch the data whenever relevant state changes.

We can force a watcher's callback to be executed immediately by passing the immediate: true option:

javascript
watch(
  source,
  (newValue, oldValue) => {
    // executed immediately, then again when `source` changes
  },
  { immediate: true }
)

Once Watchers

NOTE

This is supported only in Vue 3.4+

Watcher's callback will execute whenever the watched source changes. If you want the callback to trigger only once when the source changes, use the once: true option:

javascript
watch(
  source,
  (newValue, oldValue) => {
    // when `source` changes, triggers only once
  },
  { once: true }
)

Deep Watchers

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 beware of the performance implications.

When you use watch with a reactive object, Vue automatically creates a deep watcher - the callback will be triggered on all nested mutations:

javascript
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++

However, when you watch a getter that returns a reactive object, Vue defaults to a shallow effect (callback runs only if the returned object changes):

javascript
watch(
  () => state.someObject,
  () => {
    // fires only when state.someObject is replaced
  }
)

To watch nested properties of a getter, use the deep option:

javascript
watch(
  () => state.someObject,
  (newValue, oldValue) => {
    // Note: `newValue` will be equal to `oldValue` here
    // *unless* state.someObject has been replaced
  },
  { deep: true }
)

NOTE

In Vue 3.5+, the deep option can also be a number indicating the max traversal depth - i.e. how many levels of nested properties Vue should observe.

watchEffect()

watchEffect() is a more automatic version of watch. Rather than explicitly declaring a source, it automatically tracks all reactive state used inside the effect and re-runs when any of those dependencies change.

Unlike watch, the effect always runs immediately, so there is no need to specify immediate: true. Additionally, when working with nested data structures, watchEffect() only tracks the specific properties actually used in the effect, instead of recursively traversing the entire object. This can make it more efficient than a deep watcher in many cases.

Consider the following watch example:

javascript
watch(
  () => state.user.id,
  (id) => {
    fetchUserProfile(id)
  },
  { immediate: true }
)

The same logic can be expressed more concisely with watchEffect as follows:

javascript
watchEffect(() => {
  fetchUserProfile(state.user.id)
})

NOTE

watchEffect only tracks dependencies during its synchronous execution. When using an async callback, only reactive state accessed before the first await is tracked.

javascript
watchEffect(async () => {
  // tracked
  const id = state.user.id

  await fetchData(id)

  // NOT tracked
  console.log(state.user.name)
})

watch vs watchEffect

FeaturewatchwatchEffect
Dependency declarationExplicitAutomatic
Initial executionLazy by defaultAlways immediate
Deep watchingOptional (deep)Implicit (used properties only)
once supportYes (Vue 3.4+)No
Access to old/new valuesYesNo
Async dependency trackingYesNo (pre-await only)
Best forPrecise controlSimple side effects

Side Effect Cleanup

Watchers often trigger side effects such as API requests. A common problem occurs when the watched value changes before an asynchronous operation finishes, causing outdated callbacks to run with stale data. Consider the following example:

javascript
watch(id, (newId) => {
  fetch(`/api/${newId}`).then(() => {
    // callback logic
  })
})

If id changes before the request finishes, the old request may still run and use stale data. To avoid this, you should clean up (cancel) the previous side effect when the watcher re-runs.

Vue allows you to clean up these side effects when a watcher is invalidated.

onWatcherCleanup

NOTE

This is available only in Vue 3.5+

Using onWatcherCleanup, you can register a function that runs just before the watcher re-executes. This makes it possible to cancel pending work like network requests:

javascript
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()
  })
})

Note that this cleanup registration must happen synchronously inside the watcher callback and cannot be placed after an await statement in an async function.

onCleanup

For broader compatibility, Vue also provides a cleanup function directly to watcher callbacks and watchEffect. This approach works in older versions and is not restricted to synchronous execution:

javascript
watch(id, (newId, oldId, onCleanup) => {
  // ...
  onCleanup(() => {
    // cleanup logic
  })
})

watchEffect((onCleanup) => {
  // ...
  onCleanup(() => {
    // cleanup logic
  })
})

Callback Flush Timing

Mutating reactive state can trigger both Vue component updates and watcher callbacks that you define. Like component updates, watcher callbacks are batched by default to prevent unnecessary repeated executions. For example, if you synchronously push many items into a watched array, the watcher will usually run only once instead of firing for every mutation.

By default, a watcher's callback is called after parent component updates (if any), but before the owner component's DOM is updated. As a result, accessing the component’s own DOM inside a watcher will give you the pre-update state.

Post Watchers

If you want to access the owner component's DOM in a watcher callback after Vue has updated it, you can configure the watcher using flush: 'post' option:

javascript
watch(source, callback, {
  flush: 'post'
})

watchEffect(callback, {
  flush: 'post'
})

Post-flush watchEffect() also has a convenience alias, watchPostEffect():

javascript
import { watchPostEffect } from 'vue'

watchPostEffect(() => {
  /* executed after Vue updates */
})

Sync Watchers ​

In some cases, you may want a watcher to run immediately when reactive data changes, before Vue performs any batching or DOM updates. This can be achieved by using flush: 'sync' option:

javascript
watch(source, callback, {
  flush: 'sync'
})

watchEffect(callback, {
  flush: 'sync'
})

Sync watchEffect() also has a convenience alias, watchSyncEffect():

javascript
import { watchSyncEffect } from 'vue'

watchSyncEffect(() => {
  /* executed synchronously upon reactive data change */
})

WARNING

Sync watchers are not batched and will run on every reactive mutation. They are suitable for simple, infrequently changing values such as booleans, but should be avoided for sources that may change many times synchronously, such as arrays or large objects.

Stopping a Watcher

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 don't need to worry about stopping the watcher yourself.

The key here is that the watcher must be created synchronously. If the watcher is created in an async callback, it won't be bound to the owner component and must be stopped manually to avoid memory leaks. Here's an example:

vue
<script setup>
import { watchEffect } from 'vue'

// this one will be automatically stopped
watchEffect(() => {})

// ...this one will not!
setTimeout(() => {
  watchEffect(() => {})
}, 100)
</script>

To manually stop a watcher, use the returned handle function. This works for both watch and watchEffect:

javascript
const unwatch = watchEffect(() => {})

// ...later, when no longer needed
unwatch()

Note that there should be very few cases where you need to create watchers asynchronously, and synchronous creation should be preferred whenever possible. If you need to wait for some async data, you can make your watch logic conditional instead:

javascript
// data to be loaded asynchronously
const data = ref(null)

watchEffect(() => {
  if (data.value) {
    // do something when data is loaded
  }
})