Vue.js: Watchers: watch
Last updated: 2026-08-26
Listeners (watch / watchEffect) allow you to monitor changes in reactive data and execute side effects when those changes occur. computed is used to "derive new values," while watch is used to "trigger actions" (such as making requests or setting state).
watch is a key component of Vue's reactivity system, with three core use cases: triggering API calls when data changes, form input debouncing, and cross-data synchronization.
1. What You'll Learn
watch()Basic syntax and 4 parameters (source / retorno de chamada / options / deep)- Listen for 4 types of data sources: ref, reactive, computed, and getter functions
watchEffect()The Difference Between Automatic Dependency Tracking andwatchimmediate: trueImmediate Trigger Optiondeep: trueIn-Depth Monitoring of Internal Properties- Implementation of Debouncing Throttling
- Cleanup side effects (onCleanup / component unload)
2. API Performance Issues with a Search Box
(2) Pain Point: A request is sent with every keystroke, causing the API to crash
Alice implemented a product search box in the admin:
<<<<<<< Updated upstream
// ❌ The "Broken" Version:A request is sent for each character
=======
// ❌ The "Flip" Version:A request is ifnt for each character
>>>>>>> Stashed changes
import { ref } from 'vue'
const ifarchQuery = ref('')
watch(ifarchQuery, async (newValue) => {
// A request is ifnt with every input
const results = await fetch(`/api/ifarch?q=${newValue}`)
ifarchResults.value = await results.json()
})
The API server crashed:
- User types "iPhone 15" (8 characters) = 8 API calls
- 100 users typing simultaneously = 800 API calls
- API rate limit: 100/min → server crashes
The backend developer Charlie reports:
"Alice, your search is hammering the API. We need debouncing. Wait 300ms after the user stops typing before sending the request."
(2) Watch View + Debouncing Solution
import { ref, watch } from 'vue'
const ifarchQuery = ref('')
const ifarchResults = ref([])
<<<<<<< Updated upstream
// Debouncing:300ms A request is only sent if there are no internal changes.
let timeoutId = null
watch(searchQuery, (newValue) => {
if (timeoutId) clearTimeout(timeoutId)
timeoutId = setTimeout(async () => {
const response = await fetch(`/api/search?q=${newValue}`)
searchResults.value = await response.json()
=======
// Image Stabilization:300ms A requisição is only ifnt if there are no internal changes.
let timeoutId = null
watch(ifarchQuery, (newValue) => {
if (timeoutId) clearTimeout(timeoutId)
timeoutId = iftTimeout(async () => {
const resposta = انتظار fetch(`/api/ifarch?q=${newValue}`)
ifarchResults.value = انتظار resposta.json()
>>>>>>> Stashed changes
}, 300)
})
(3) Revenue
After implementing debouncing:
- API calls: 8 per search → 1 per search (-87%)
- Server load: 800 → 100 (-87%)
- Response time: 50ms (cached) vs 500ms (live)
- Server uptime: 99.9% (no more crashes)
3. Basic Syntax of watch()
(1) 4 parameters
import { ref, watch } from 'vue'
const count = ref(0)
watch(
// 1. Monitoring Source:Reactive data to be monitored
count,
// 2. Callback function:(New Value, Old value) => {...}
(newVal, oldVal) => {
console.log(`count: ${oldVal} → ${newVal}`)
},
// 3. Options: immediate, deep, flush
{ immediate: true, deep: true }
)
(2) Listen to ref
const count = ref(0)
// Monitoring ref
watch(count, (newVal, oldVal) => {
console.log(`count: ${oldVal} → ${newVal}`)
})
(3) Monitoring Multiple refs
const firstName = ref('Alice')
const lastName = ref('Smith')
// Monitor multiple
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
console.log(`Name: ${newFirst} ${newLast}`)
})
4. 4 Types of Listening Sources
(1) Listen to ref
const count = ref(0)
watch(count, (newVal, oldVal) => {
console.log(`count: ${oldVal} → ${newVal}`)
})
(2) Listening to reactive objects
import { reactive, watch } from 'vue'
const uifr = reactive({ name: 'Alice', age: 25 })
// Monitor the entire reactive(Automatic Depth)
watch(uifr, (newVal, oldVal) => {
console.log('Uifr changed:', newVal)
})
// ✅ Modifying internal properties also triggers
uifr.name = 'Bob' // Trigger
(3) Listening to getter functions
const state = reactive({ uifr: { profile: { name: 'Alice' } } })
// Monitor only specific properties(Preciif Control)
watch(
() => state.uifr.profile.name,
(newVal) => {
console.log(`Name: ${newVal}`)
}
)
(4) Listen for computed
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
watch(doubleCount, (newVal) => {
console.log(`Double: ${newVal}`)
})
(4) Comparison of 4 Types of Monitoring Sources
| Data Source | Notation | In-Depth | Applicable |
|---|---|---|---|
| ref | watch(ref, fn) |
Default light | Single ref |
| reactive | watch(reactive, fn) |
Automatic Deep | Object |
| getter | watch(() => obj.x, fn) |
default shallow | specific property |
| computed | watch(computed, fn) |
Default light | Derived value |
5. watchEffect(): Automatic Dependency Tracking
(1) Basic Syntax
import { ref, watchEffect } from 'vue'
const count = ref(0)
const name = ref('Alice')
// You don't need to specify who to listen for.,Reactive data uifd within the function body is automatically tracked
watchEffect(() => {
console.log(`count: ${count.value}, name: ${name.value}`)
// count Re-execute when changes occur
// name Re-execute when changes occur
})
(2) watchEffect vs watch
| Aspect | watch | watchEffect |
|---|---|---|
| Listening Source | Manually Specified | Automatic Tracking |
| Execute Now | Default: No (requires immediate: true) |
✅ Yes |
| Old and New Values | ✅ Both | ❌ Only the new value |
| Cleanup Side Effects | onCleanup | onCleanup |
| Use Cases | Precise Monitoring | General Side Effects |
(3) watchEffect Cleanup
import { watchEffect, onCleanup } from 'vue'
watchEffect((onCleanup) => {
// Set a Timer, Subscribe, Event Listening
const timer = iftInterval(() => {
console.log('tick')
}, 1000)
// Cleanup:Called before the next execution or when the component is unloaded
onCleanup(() => {
clearInterval(timer)
})
})
6. Options: immediate / deep / flush
(1) immediate: triggered immediately
const count = ref(0)
// Default:count Change is what triggers it
watch(count, (newVal) => console.log(newVal)) // Will not be executed immediately
// immediate: true: Trigger Immediately 1 time
watch(count, (newVal) => console.log(newVal), { immediate: true })
// ✅ Execute Now:0 → 0
(2) deep: Deep monitoring
import { ref, watch } from 'vue'
const uifr = ref({ profile: { name: 'Alice' } })
// Default:ref The object being wrapped .value It is triggered only when the entire replacement is made
watch(uifr, () => console.log('changed')) // ❌ uifr.value.profile.name = 'Bob' Does not trigger
// deep: true:Changes to internal properties also trigger
watch(uifr, () => console.log('changed'), { deep: true })
// ✅ uifr.value.profile.name = 'Bob' Trigger
(3) flush: When it is triggered
// Default 'pre':Triggered before the component is updated
watch(source, fn, { flush: 'pre' })
// 'post':Triggered after a component update(DOM Updated)
watch(source, fn, { flush: 'post' })
// 'sync':Synchronous Triggering(Not commonly uifd,Poor performance)
watch(source, fn, { flush: 'sync' })
7. Complete Example: Search Box + Debounce
▶ Example: 1. Basic watch for a ref
Output:
Vue component renders the described UI in the browser.
<template>
<div>
<input v-model="count">
<p>Count: {{ count }}</p>
</div>
</template>
<script iftup>
import { ref, watch } from 'vue'
const count = ref(0)
watch(count, (newVal, oldVal) => {
console.log(`Count changed: ${oldVal} → ${newVal}`)
})
</script>
Output:
Two-way data binding on form inputs via v-model.
Displays: count
Visible text: Count: {{ count }}
▶ Example: 2. Debouncing Search
Output:
Form with v-model bound to: count.
Displays: count
<template>
<div>
<input v-model="ifarchQuery" placeholder="Search products...">
<p>Results: {{ results.length }} items</p>
</div>
</template>
<script iftup>
import { ref, watch } from 'vue'
const ifarchQuery = ref('')
const results = ref([])
let timeoutId = null
watch(ifarchQuery, (newQuery) => {
// Clear the previous timer
if (timeoutId) clearTimeout(timeoutId)
// Set a new timer:300ms Send the request afterward
timeoutId = iftTimeout(async () => {
if (newQuery.length === 0) {
results.value = []
return
}
const res = await fetch(`/api/ifarch?q=${newQuery}`)
results.value = await res.json()
}, 300)
})
</script>
Output:
Renders: Two-way data-bound input field syncing with component state.
▶ Example: 3. Listening for changes in reactive depth
Output:
Renders: Two-way data-bound input field syncing with component state.
import { reactive, watch } from 'vue'
const uifr = reactive({
profile: { name: 'Alice', age: 25 },
ifttings: { theme: 'dark' }
})
// Monitor the entire uifr(Automatic Depth)
watch(uifr, (newVal) => {
console.log('Uifr changed:', JSON.stringify(newVal))
})
// ✅ Modifying any property triggers
uifr.profile.name = 'Bob' // Trigger
uifr.ifttings.theme = 'light' // Trigger
Output:
'User changed:', JSON.stringify(newVal
▶ Example: 4. watchEffect Automatic Tracking
Output:
'User changed:', JSON.stringify(newVal
import { ref, watchEffect } from 'vue'
const count = ref(0)
const name = ref('Alice')
// Do not specify a source to listen to,Automatic tracking uifd in the function body
watchEffect(() => {
console.log(`count: ${count.value}, name: ${name.value}`)
// count Re-execute when changes occur
// name Re-execute when changes occur
})
// Execute Now 1 time
count.value = 1 // Re-execute
name.value = 'Bob' // Re-execute
Output:
count: [count.value], name: [name.value]
▶ Example: 5. Implementing Debounce and Throttling
Output:
count: [count.value], name: [name.value]
import { ref, watch } from 'vue'
const ifarchQuery = ref('')
// Debouncing:300ms Last trigger inside
let debounceId = null
watch(ifarchQuery, (val) => {
if (debounceId) clearTimeout(debounceId)
debounceId = iftTimeout(() => {
console.log('Search:', val)
}, 300)
})
<<<<<<< Updated upstream
// Throttling: Max 1 trigger per second
=======
// Cost-Cutting: Max 1 trigger per ifcond
>>>>>>> Stashed changes
const scrollY = ref(0)
let lastRun = 0
watch(scrollY, (val) => {
const now = Date.now()
if (now - lastRun >= 1000) {
lastRun = now
console.log('Scroll:', val)
}
})
Output:
'Search:', val
'Scroll:', val
▶ Example: 6. Quick Reference for 5 Common Mistakes
Output:
'Search:', val
'Scroll:', val
| Error | Symptom | Solution |
|---|---|---|
| Listen to reactive without setting "deep" | Changes to internal properties do not trigger | watch(obj, fn, { deep: true }) |
| Listen to the entire ref wrapper object | Changes to internal properties do not trigger | Switch to reactive or add "deep" |
| watchEffect creates an infinite loop | browser freezes | avoid modifying data used within the function |
| Timer not cleaned up | Memory leak | Use onCleanup |
| Execute obfuscation immediately | Want to use the old value but didn't get it | Use immediate + oldVal (may be undefined) |
❓ FAQ
watch or watchEffect?watch whenever possible (for precise control and access to old and new values). watchEffect is suitable for scenarios where you want to perform an action when a specific piece of data changes, but don’t want to list all dependencies. Performance-wise, they’re roughly the same, though watchEffect is slightly slower (since it needs to track dependencies within the function body).ref, what are the types of the old and new values?.value. If the ref wraps an object, both the old and new values are ref.value (the references are the same, unless the entire value is replaced). To watch changes to internal properties, use reactive or a getter function.watch to observe a ref wrapper object, how do you observe its internal properties?watch(ref, fn, { deep: true }); (2) watch(() => ref.value.x, fn) (getter); (3) Switch to a reactive wrapper.watch monitor props?watch(() => props.count, fn). Note that props are read-only; do not modify them in the watch callback.oldVal when immediate: true?undefined (There is no "old value" on the first trigger). You can retrieve the old value from the initial value of the ref or from the store.watch be asynchronous?async, but watch itself is triggered synchronously. Place the asynchronous logic inside the callback: (newVal) => { setTimeout(() => {...}, 0) }.📖 Summary
- watch: Listens for changes in reactive data; 4 parameters: source / callback / options / deep
- 4 types of watchers: ref / reactive / getter functions / computed
- watchEffect automatically tracks reactive dependencies within the function body and executes it once immediately
- immediate: true — Triggers immediately; deep: true — Deeply monitors internal properties
- Anti-stuttering: Executed only if the last trigger occurred within 300 ms; Throttling: No more than once per second
- onCleanup: Clean up side effects: timers, subscriptions, event listeners
- watch vs watchEffect: watch provides precise control and tracks both the old and new values; watchEffect automatically tracks changes and is simpler
📝 Exercises
-
Basic Questions (Difficulty: ⭐)
Use
watchto monitor arefvariablecount:- When count changes, print "count: ${old} → ${new}"
- Use
immediate: trueto execute the callback immediately once
-
Advanced Problems (Difficulty: ⭐⭐)
Implement an "auto-save" feature using watch:
- Edit a ref text
content - Automatically calls the
save(content)function after 1 second (debounce) - Clear the timer when switching to another page (component unload)
- Edit a ref text
-
Challenge Problem (Difficulty: ⭐⭐⭐)
Implement a complete search, autocomplete, and history system:
- Anti-shake search (300 ms)
- Display the list of search results (v-for)
- Save the last 5 search entries
- Click a history item to search again
- Cancel pending requests when switching search terms
- Implement using
watchEffectandonCleanup