Watchers let you run side effects in response to reactive data changes - things that computed properties can't do: async operations, DOM manipulation, calling external APIs, or logging.
| Feature | watch() | watchEffect() |
|---|---|---|
| Source declaration | Explicit - you specify what to watch | Automatic - tracks all accessed refs |
| Runs immediately | No (unless immediate: true) | Yes - runs on creation |
| Old value access | Yes - (newVal, oldVal) | No |
| Best for | Specific data changes, need old value | Multiple deps, immediate execution |
<template>
<div>
<input v-model="query" placeholder="Search..." />
<input v-model.number="userId" type="number" placeholder="User ID" />
<p>{{ status }}</p>
</div>
</template>
<script setup>
import { ref, reactive, watch, watchEffect, onUnmounted } from 'vue'
const query = ref('')
const userId = ref(1)
const status = ref('Ready')
const user = reactive({ name: '', email: '' })
// 1. Watch a single ref
watch(query, (newVal, oldVal) => {
console.log(`Query changed: "${oldVal}" -> "${newVal}"`)
status.value = `Searching for: ${newVal}`
})
// 2. Watch with options
watch(userId, async (newId) => {
status.value = 'Loading...'
const res = await fetch(`/api/users/${newId}`)
const data = await res.json()
user.name = data.name
user.email = data.email
status.value = 'Loaded'
}, {
immediate: true, // run immediately on mount
flush: 'post', // run after DOM updates
})
// 3. Watch multiple sources
watch([query, userId], ([newQuery, newId], [oldQuery, oldId]) => {
console.log('Either changed:', newQuery, newId)
})
// 4. Watch reactive object - need getter or deep: true
const form = reactive({ name: '', email: '' })
// Watch specific property with getter
watch(() => form.name, (newName) => {
console.log('Name changed:', newName)
})
// Watch entire reactive object (deep)
watch(form, (newForm) => {
console.log('Form changed:', newForm)
}, { deep: true })
// 5. watchEffect - auto-tracks dependencies
const stop = watchEffect(() => {
// Automatically tracks query.value and userId.value
document.title = `${query.value} | User ${userId.value}`
console.log('Effect ran')
})
// 6. watchEffect with cleanup
watchEffect((onCleanup) => {
const timer = setTimeout(() => {
console.log('Debounced:', query.value)
}, 500)
onCleanup(() => clearTimeout(timer)) // cleanup before next run
})
// 7. Stop a watcher manually
onUnmounted(() => stop()) // stop watchEffect when component unmounts
</script>
<template>
<div>
<input v-model="searchQuery" placeholder="Search users..." />
<p v-if="loading">Searching...</p>
<ul v-else>
<li v-for="user in results" :key="user.id">{{ user.name }}</li>
<li v-if="results.length === 0 && searchQuery">No results</li>
</ul>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const searchQuery = ref('')
const results = ref([])
const loading = ref(false)
// Debounced search with watch + cleanup
watch(searchQuery, (newQuery, _, onCleanup) => {
if (!newQuery.trim()) {
results.value = []
return
}
loading.value = true
// AbortController to cancel previous request
const controller = new AbortController()
const timer = setTimeout(async () => {
try {
const res = await fetch(`/api/users?q=${newQuery}`, {
signal: controller.signal
})
results.value = await res.json()
} catch (err) {
if (err.name !== 'AbortError') console.error(err)
} finally {
loading.value = false
}
}, 400) // 400ms debounce
// Cleanup: cancel request and clear timer if query changes
onCleanup(() => {
clearTimeout(timer)
controller.abort()
loading.value = false
})
})
</script>
watch accepts refs, reactive objects, getters, or arrays of sources. Pass a getter when observing one property of a reactive object. A deep watcher traverses nested state and can be expensive, so prefer a focused source or immutable replacement when possible.
The default watcher runs before the component DOM update. Use post timing when the callback must inspect updated DOM, and use sync only for a narrow case that cannot tolerate batching. Select timing from the effect requirement instead of adding nextTick calls blindly.
A watched search term can change again before its request finishes. Register cleanup that aborts or invalidates the older operation, then allow only the latest result to update state. Test rapid changes, rejection, unmount, and an empty source value so stale data cannot reappear.
watchEffect automatically tracks every reactive value read during its execution. That is convenient, but it can capture extra dependencies without you noticing. watch is more explicit because you name the source being observed.
When a watched value starts an async request, an older request can finish after a newer one and overwrite the latest state. Use watcher cleanup to cancel or invalidate the previous request.
Use flush: post when the watcher needs the DOM after Vue has applied the latest reactive update. Measuring height, focusing a newly rendered element, or reading rendered text can be wrong if the watcher runs before the DOM update.
Explore 500+ free tutorials across 20+ languages and frameworks.