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:
- A data source you want to watch (can be a ref, or a getter function that returns a value).
- A callback function that executes when the data source changes. The callback function receives the new value and old value as parameters.
- An optional configuration object.
<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.
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.
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.
<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:
watchonly tracks explicitly specified data sources, whilewatchEffectautomatically tracks all reactive data accessed in its callback function. - Lazy Execution:
watchis lazy by default (only executes when the data source changes), whilewatchEffectexecutes once immediately when created. - Data Access:
watchcan access both the new and old values, whilewatchEffectcannot.
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.