Vue Watchers

The watch option lets a component react to a specific piece of state changing, giving you both the old and new value to act on.

What Watchers Are For

A watcher observes a reactive source and runs a callback whenever that source changes. Watchers are the right tool for side effects that a computed property should never perform - logging, calling an API, syncing a value to localStorage, or resetting some unrelated piece of state in response to a change.

Declaring a Basic Watcher

The watch option is an object whose keys match the name of a data property, prop, or computed property. Each value is a handler function that Vue calls with two arguments: the new value first, then the old value.

A basic watcher with old and new values

<div id="app">
  <input v-model="searchTerm" placeholder="Search users..." />
  <p>Last search logged: {{ lastLogged }}</p>
</div>

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
  const app = Vue.createApp({
    data() {
      return {
        searchTerm: '',
        lastLogged: ''
      }
    },
    watch: {
      searchTerm(newValue, oldValue) {
        console.log('Search changed from "' + oldValue + '" to "' + newValue + '"')
        this.lastLogged = newValue
      }
    }
  })
  app.mount('#app')
</script>

Watching Nested Data with deep and immediate

By default, watch only detects a fresh assignment to the source - reassigning profile = { ...profile, age: 31 } triggers it, but mutating profile.age directly does not. Setting deep: true tells Vue to walk into nested objects and arrays and fire the handler on any mutation inside them. Setting immediate: true runs the handler once right away when the watcher is created, not only after the first future change.

Object form with deep and immediate

<div id="app">
  <button @click="profile.age++">Birthday (+1 year)</button>
  <p>Age: {{ profile.age }}</p>
  <p>Change log entries: {{ changeLog.length }}</p>
</div>

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
  const app = Vue.createApp({
    data() {
      return {
        profile: { name: 'Sam', age: 30 },
        changeLog: []
      }
    },
    watch: {
      profile: {
        handler(newProfile) {
          this.changeLog.push('Profile updated, age is now ' + newProfile.age)
        },
        deep: true,
        immediate: true
      }
    }
  })
  app.mount('#app')
</script>
  • A watcher's handler always receives two arguments: the new value first, then the old value.
  • By default, watch only detects a fresh assignment to the source; mutating properties inside an object requires deep: true.
  • Setting immediate: true runs the handler once right away, in addition to every future change.
  • You can watch a dotted-path string like 'settings.theme.fontSize' to observe one nested property without watching the whole object.
  • Computed properties can be watched too, since they are just another reactive source.

Watching a Nested Path and a Computed Property

Instead of deep-watching an entire object, you can watch a single nested value by passing its dotted path as a string key. Watch sources are not limited to data properties either - a computed property is a perfectly valid watch target, since it is reactive just like any other source.

Watching a dotted path and a computed property

<div id="app">
  <input v-model.number="settings.theme.fontSize" type="number" />
  <p>Font size doubled: {{ doubledFontSize }}</p>
</div>

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
  const app = Vue.createApp({
    data() {
      return {
        settings: { theme: { fontSize: 14 } }
      }
    },
    computed: {
      doubledFontSize() {
        return this.settings.theme.fontSize * 2
      }
    },
    watch: {
      'settings.theme.fontSize'(newSize, oldSize) {
        console.log('Font size changed from', oldSize, 'to', newSize)
      },
      doubledFontSize(newVal) {
        console.log('Doubled font size is now', newVal)
      }
    }
  })
  app.mount('#app')
</script>
Note: Tip: this.$watch('someProperty', callback) inside a lifecycle hook creates a watcher imperatively and returns a stop function you can call to remove it later - handy for temporary watchers tied to a specific condition.
OptionPurposeDefault
handlerThe function that runs when the watched source changesrequired
deepDetects mutations inside nested objects/arrays, not just reassignmentfalse
immediateRuns the handler once immediately when the watcher is createdfalse
flushControls timing relative to Vue's DOM update, e.g. 'post' runs after DOM updates'pre'
Note: Warning: reach for watch only when you need a side effect - console logging, network requests, writing to localStorage, resetting unrelated state. If you just need a derived value to display, a computed property is simpler and self-cleaning.

Exercise: Vue Watchers

When does a watch callback actually execute relative to the watched data changing?