Vue.js 3 Watchers Tutorial

In this Vue tutorial we learn how to watch data and computed properties with watchers and execute code in response to any changes.

We cover how to watch deep objects and arrays, run watchers on page load and when to use watchers instead of computed properties.

Lesson Project

If you want to follow along with the examples in this lesson, you will need an app generated by the Vue CLI . If you already have one from the previous lesson, you can use it instead.

What is a Watcher

A watcher allows us to monitor a piece of data, or computed property, and execute code in response to a change in that data.

Watchers are similar to computed properties but their use cases are different. Before we get into that however, let’s see how to use watchers.

How to define and use a Watcher

We define a watcher as a function in the watch option of the component’s config object. Vue requires the watcher method name to be the same as the data property we want to watch.

Syntax: watcher
watch: {
  propertyToWatch(newValue, oldValue) {
    // non-returning logic
  }
}

A watcher has access to the most recent value ( newValue ) and the previous value ( oldValue ) of the data property. It will monitor the data property and execute the logic in the watcher each time it receives a new value.

note A watcher does not return any value. It only works on other data, we don’t use it like a regular method in the template.

To demonstrate, let’s create a counter example that increments or decrements a number when the user clicks a button. We’ll use a watcher to check when the number gets to 5, then show an alert.

Example: src/App.vue
<template>
  <p>Counter: {{ count }}</p>

  <button @click="count++">Increment</button>
  <button @click="count--">Decrement</button>
</template>

<script>
export default {
  data() {
    return {
      count: 0
    }
  },
  watch: {
    count(newValue) {
      if (newValue === 5) alert('Value reached 5')
    }
  }
}
</script>

When we run the example and increment the number to 5, it shows the alert.

Although the example above works fine, there is a potential issue.

If the number is above 5 and we click the ‘Decrement’ button, it will display the console log message again when it comes back to 5.

Let’s imagine that the number is a volume control. We want to alert the user that listening to a volume above 5 may damage their hearing. But we don’t want to display the alert message when the user is already above 5 and turning the volume down.

As mentioned earlier, Vue automatically gives us the previous value of the data property as well. We can use it to check if the new value is greater than the old value. If so, the user is turning the volume up and only then do we display the alert.

Let’s modify the example above and check if newValue is greater than oldValue .

Example: src/App.vue
<template>
  <p>Counter: {{ count }}</p>

  <button @click="count++">Increment</button>
  <button @click="count--">Decrement</button>
</template>

<script>
export default {
  data() {
    return {
      count: 0
    }
  },
  watch: {
    count(newValue, oldValue) {
      if (newValue > oldValue && newValue === 5)
        alert('Increasing the volume past 5 may damage your hearing')
    }
  }
}
</script>

The message will display when the number reaches 5. But if we go past and decrease it again, the message won’t show.

Watchers vs Computed Properties

Because watchers and computed properties monitor changes in data, a question that often comes up is when to use which.

Watchers provide a more generic way to react to data than computed properties. So, technically we can use watchers instead of computed properties, but it’s not recommended.

To demonstrate, we will revisit our full name example from the lesson on computed properties .

Example: src/App.vue
<template>
  <p>Full Name: {{ fullName }}</p>
  <p>
    <label for="firstName">First Name: </label>
    <input id="firstName" type="text" v-model="firstName">
  </p>
  <p>
    <label for="lastName">Last Name: </label>
    <input id="lastName" type="text" v-model="lastName">
  </p>
</template>

<script>
export default {
  data() {
    return {
      firstName: '',
      lastName: ''
    }
  },
  computed: {
    fullName() {
      return this.firstName + ' ' + this.lastName
    }
  }
}
</script>

In the example we have two input fields that take a first and last name from the user and store them into their respective data properties. A computed property then combines the two names into a full name and displays it in a paragraph in the template.

We’ll change the logic to use a watcher instead of a computed property. But, this is where we’ll run into some complications.

  • A watcher can only monitor a single data property. To watch both the first and last names, we will need to use two watchers.
  • A watcher cannot return a value, so we will need another data property to store the concatenated names.

Keeping the rules above in mind, let’s modify the example.

Example: src/App.vue
<template>
  <p>Full Name: {{ fullName }}</p>
  <p>
    <label for="firstName">First Name: </label>
    <input id="firstName" type="text" v-model="firstName">
  </p>
  <p>
    <label for="lastName">Last Name: </label>
    <input id="lastName" type="text" v-model="lastName">
  </p>
</template>

<script>
export default {
  data() {
    return {
      firstName: '',
      lastName: '',
      fullName: ''
    }
  },
  watch: {
    fistName(newValue) {
      this.fullName = newValue + ' ' + this.lastName;
    },
    lastName(newValue) {
      this.fullName = this.firstName + ' ' + newValue;
    }
  }
}
</script>

If we run the example above in the browser, it works the same as the computed property example.

Even though this approach works, it’s not ideal to use in this situation.

  • More code increases development time.
  • More code is harder to test and debug.
  • It’s more difficult to read and see at a glance what is happening.

When to use a Watcher vs a Computed Property

Now that we know more about the differences, we can answer the question: when do we actually use which?

Use Computed Properties when:

  • You need to compose new data from more than one existing data sources. An example would be when we created a full name from the first and last name data properties.
  • You need to reduce the length of a variable. An example would be when we need to access a deeply nested property in an object and bind it to the View. It will make our template less cluttered.

Use Watchers when:

  • You want to check if a data or computed property has changed to a specific value in order to know if you are ready to perform an action. An example would be when we displayed an alert once the number in our counter reached 5.
  • You have to call an API in response to a change in data. An example would be a user clicking a button to receive a new inspirational quote, or upcoming events in their area etc.

How to run a Watcher on page load with immediate

By default, a watcher will not run when a page first loads. However, many times in an application you will want to display data from an API immediately.

As an example, let’s consider a film review application. Until the user performs a search, we don’t know which film they want to see a review for. So we want to show the most popular reviews of the latest film on the home page.

Let’s create a simplified version of such an application and simulate the API call with a console log.

Example: src/App.vue
<template>
  <p>Search for a film: <input type="text" v-model="filmName"></p>
</template>

<script>
export default {
  data() {
    return {
      filmName: ''
    }
  },
  watch: {
    filmName(newValue) {
      console.log('Calling API for ' + newValue)
    }
  }
}
</script>

If we run the example in the browser and type a name, Vue will log the message with each keypress to the console. So everything works as expected.

Now let’s add a default value to the filmName data property. The idea here is that because we use the v-model directive, the default value we specify will show up in the input field. And because it’s in the input field, it should activate the watcher and print the message to the console.

Example: src/App.vue
<template>
  <p>Search for a film: <input type="text" v-model="filmName"></p>
</template>

<script>
export default {
  data() {
    return {
      filmName: 'Rabobi'
    }
  },
  watch: {
    filmName(newValue) {
      console.log('Calling API for ' + newValue)
    }
  }
}
</script>

If we run the example in the browser, the new default film name shows up in the input field. However, the console doesn’t display a message, indicating that the watcher didn’t activate.

That is the default behavior for a watcher. If we want it to run when the page loads, we need to set the immediate option of the watcher to true.

This also means we need to change the syntax of the watcher method. The method becomes an object with a function called handler that contains the watcher logic. Alongside the handler we use the immediate property with a value of true.

Syntax: watcher object syntax
watch: {
  dataPropertyToWatch: {
    handler(newValue) {
      // watcher method logic
    },
    immediate: true
  }
}

To demonstrate, let’s make the changes to our example.

Example: src/App.vue
<template>
  <p>Search for a film: <input type="text" v-model="filmName"></p>
</template>

<script>
export default {
  data() {
    return {
      filmName: 'Spiderman'
    }
  },
  watch: {
    filmName: {
      handler(newValue) {
        console.log('Calling API for ' + newValue)
      },
      immediate: true
    }
  }
}
</script>

Now when we run the example in the browser, it will print the message to the console on the page load.

So if we did reach out to an actual API, it would be able to load the reviews for Spiderman when the page loads.

How to watch nested objects with deep

Many times in an application, you will want to watch a value in a nested object. But by default, Vue doesn’t watch nested data properties and requires us to set the deep option of the watcher to true.

Syntax: deep
watch: {
  dataPropertyToWatch: {
    handler(newValue) {
      // watcher method logic
    },
    deep: true
  }
}

As an example, let’s modify our film example from earlier to use a nested data property object. We’ll also change the watcher and set its deep property to true.

Example: src/App.vue
<template>
  <p>Title:    <input type="text" v-model="film.title"></p>
  <p>Director: <input type="text" v-model="film.director"></p>
</template>

<script>
export default {
  data() {
    return {
      film: {
        title: '',
        director: ''
      }
    }
  },
  watch: {
    film: {
      handler(newValue) {
        console.log('Film: ' + newValue.title + ', directed by ' + newValue.director)
      },
      deep: true
    }
  }
}
</script>

If we run the example in the browser and type some data into the text fields, the console will show a message on each keypress.

Vue allows us to have both the deep and immediate properties active at the same time. It doesn’t matter in which order the properties are defined. As long as they exist, Vue can use them.

Example: src/App.vue
<template>
  <p>Title:    <input type="text" v-model="film.title"></p>
  <p>Director: <input type="text" v-model="film.director"></p>
</template>

<script>
export default {
  data() {
    return {
      film: {
        title: 'Spiderman',
        director: 'Jon Watts'
      }
    }
  },
  watch: {
    film: {
      handler(newValue) {
        console.log('Film: ' + newValue.title + ', directed by ' + newValue.director)
      },
      deep: true,
      immediate: true
    }
  }
}
</script>

How to watch arrays with deep

Vue counts arrays as deeply nested. So when we’re working with an array, we also have to specify and set the deep option to true.

As an example, let’s create an list of films in an array with the option to add more by clicking a button. To keep things simple, we’ll use the Javascript array push method directly in the click listener.

Example: src/App.vue
<template>
  <button @click="filmList.push('Aquaman')">Add film</button>
</template>

<script>
export default {
  data() {
    return {
      filmList: ['Spiderman', 'Batman']
    }
  },
  watch: {
    filmList: {
      handler(newValue) {
        console.log('New list ' + newValue);
      },
      deep: true
    }
  }
}
</script>

When we press the button, the new film is added to the array and logged in the console.

It’s worth it to note that if we add the immediate property, it will display the list with default values only. It will not add “Aquaman” to the array until the click event fires.

Example: src/App.vue
<template>
  <button @click="filmList.push('Aquaman')">Add film</button>
</template>

<script>
export default {
  data() {
    return {
      filmList: ['Spiderman', 'Batman']
    };
  },
  watch: {
    filmList: {
      handler(newValue) {
        console.log('New list ' + newValue);
      },
      deep: true,
      immediate: true
    }
  }
}
</script>

If we run the example above, the array is displayed with the two initial values. The new value will only be added once we click the button and the click event fires.