Skip to content

Vue Watchers

Although computed properties are more suitable in most cases, sometimes we need to perform "side effects" when state changes: for example, modifying the DOM, or modifying state elsewhere based on results of asynchronous operations. In Vue, we can use watchers to respond to data changes.

watch

The watch function is used to execute a callback function when a reactive data source changes.

Basic Usage

watch accepts three parameters:

  1. A data source you want to watch (can be a ref, or a getter function that returns a value).
  2. A callback function that executes when the data source changes. The callback function receives the new value and old value as parameters.
  3. An optional configuration object.
vue
<script setup>
import { ref, watch } from 'vue'

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

// Can directly watch a ref
watch(question, async (newQuestion, oldQuestion) => {
  if (newQuestion.indexOf('?') > -1) {
    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
    }
  }
})
</script>

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

watch vs. computed

Both watch and computed allow us to perform operations reactively. The main difference is their use cases:

  • computed: Used to derive new data. It is declarative, returns a value, and has caching. Use computed properties when you need to calculate a new value based on other data.
  • watch: Used to perform side effects. It is imperative and doesn't return a value. Use watchers when you need to perform asynchronous operations, manipulate the DOM, or interact with other systems when data changes.

Deep Watching

Directly passing a reactive object to watch() implicitly creates a deep watcher—the callback function will be triggered on all nested changes. However, this only triggers when the entire object is replaced.

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

watch(obj, (newValue, oldValue) => {
  // Triggers when obj.count changes
})

If you want to trigger when internal properties of an object change, you need to use the deep: true option.

js
watch(
  () => state.someObject,
  (newValue, oldValue) => {
    // ...
  },
  { deep: true }
)

watchEffect

watchEffect is a variant of watch. It executes the callback function immediately once, and then automatically tracks all its reactive dependencies. When any of the dependencies changes, it re-runs the function.

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

const todoId = ref(1)
const data = ref(null)

watchEffect(async () => {
  // This effect runs immediately,
  // then re-runs when todoId.value changes.
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
  )
  data.value = await response.json()
})
</script>

<template>
  <p>Todo id: {{ todoId }}</p>
  <button @click="todoId++">Fetch next todo</button>
  <p v-if="!data">Loading...</p>
  <pre v-else>{{ data }}</pre>
</template>

watch vs. watchEffect

  • Dependency Tracking: watch only tracks explicitly specified data sources, while watchEffect automatically tracks all reactive data accessed in its callback function.
  • Lazy Execution: watch is lazy by default (only executes when the data source changes), while watchEffect executes once immediately when created.
  • Data Access: watch can access both the new and old values, while watchEffect cannot.

Which one to choose depends on your needs. If you need precise control over the data to watch, or need to access the old value, use watch. If your side effect logic depends on multiple reactive sources and you don't need the old value, watchEffect will be more concise.

Content is for learning and research only.