Listener Watch

In modern Vue application development, managing reactive state changes often requires executing "side effects" such as triggering asynchronous network requests, directly interacting with the DOM, or synchronizing data with local storage. While computed properties handle most derived data scenarios gracefully, the watcher (watch) serves as the ideal tool whenever you need to perform asynchronous or heavy computational tasks in response to data fluctuations.

A watcher is a reactive data monitoring mechanism provided by Vue. Its core philosophy revolves around registering a callback function for specific reactive sources; whenever those sources change, Vue automatically executes the callback while passing the newly updated value and the previous value as parameters.

Typical use cases for watchers include:

  • Executing asynchronous operations following data changes (such as debounced search queries)
  • Performing expensive or time-consuming tasks based on state transitions
  • Comparing and evaluating differences between old and new values
  • Tracking route mutations, form inputs, or global state transitions
    In the Composition API, you can establish a watcher by importing the watch function from vue:
<script setup>
import { ref, watch } from 'vue'

const count = ref(0)

watch(count, (newVal, oldVal) => {
  console.log(`Count changed from ${oldVal} to ${newVal}`)
})
</script>

<template>
  <button @click="count++">Clicked {{ count }} times</button>
</template>

The callback function accepts two primary arguments: the new value followed by the old value. In the Options API, watchers are declared directly inside the watch option object:

export default {
  data() {
    return { count: 0 }
  },
  watch: {
    count(newVal, oldVal) {
      console.log(newVal, oldVal)
    }
  }
}

Watching Properties of Reactive Objects

When attempting to watch a specific property inside a reactive object, passing the property value directly will not work because primitive types are passed by value, failing to establish a reactive link. The correct approach is to supply a getter function as the data source:

import { reactive, watch } from 'vue'

const user = reactive({ name: 'John', age: 18 })

// Correct: Using a getter function
watch(() => user.name, (newVal, oldVal) => {
  console.log(`Name changed from ${oldVal} to ${newVal}`)
})

// Incorrect: watch(user.name, ...) will not trigger properly

Deep Watchers

When tracking nested properties inside an object, a standard getter function only detects changes at the reference level. To capture mutations deep within nested structures, you can enable the deep option:

const state = reactive({
  profile: { city: 'Beijing' }
})

watch(
  () => state.profile,
  (newVal) => {
    console.log('Profile updated:', newVal.city)
  },
  { deep: true }
)

Keep two crucial considerations in mind:

  • Passing a reactive object directly into watch implicitly enables deep watching, where both the new and old values point to the same object reference.
  • Deep watching requires traversing all nested properties, which can introduce performance overhead on large data structures and should be used judiciously.

Common Options

Immediate Execution with immediate

By default, watcher callbacks are lazy—they only trigger after the monitored source changes. If you need the callback to run immediately upon component creation, set immediate: true:

watch(
  keyword,
  (newVal) => {
    fetchSearchResult(newVal)
  },
  { immediate: true }
)

This proves exceptionally useful in scenarios requiring initial data fetching on load, followed by subsequent re-fetches upon user input.

Watching Multiple Sources

The first argument of watch can also accept an array, allowing you to monitor multiple reactive sources simultaneously:

const firstName = ref('Doe')
const lastName = ref('John')

watch(
  [firstName, lastName],
  ([newFirst, newLast], [oldFirst, oldLast]) => {
    console.log(`Full name updated`)
  }
)

Comparing watch and watchEffect

Vue 3 also provides watchEffect, which differs from watch in several key aspects:

  • Dependency Tracking: watch requires explicitly declared data sources; watchEffect automatically tracks every reactive dependency accessed within its callback.
  • Execution Timing: watch is lazy by default; watchEffect executes immediately upon creation.
  • Value Access: watch provides access to both new and old values; watchEffect does not provide old values.
watchEffect(() => {
  // Automatically tracks count.value and runs immediately
  console.log('Current count:', count.value)
})

Generally speaking, use watch when you need old values or precise multi-source control, and opt for watchEffect when dealing with straightforward dependencies where automatic collection is preferred.

Stopping Watchers and Best Practices

Watchers created synchronously inside <script setup> automatically stop when the host component unmounts. However, if a watcher is initialized inside an asynchronous callback (such as a setTimeout), it must be stopped manually:

const stop = watch(count, callback)

// Manually stop the watcher when no longer needed
stop()

Best Practice Summary

  1. Prefer computed properties for derived data, reserving watch strictly for side effects.
  2. Avoid overusing deep: true to prevent unnecessary performance degradation.
  3. Always use a getter function when watching properties of reactive objects.
  4. Remember to manually stop watchers created within asynchronous workflows to prevent memory leaks.