Chapter 69: Vue $watch() Method
Watch()).
This is the tool you reach for when you want to run some code automatically every time a specific piece of reactive data changes.
It’s like saying:
“Hey Vue, please keep an eye on this value/ref/computed/getter… and the moment it changes (even deeply nested changes), run my callback function.”
In Vue 2 / early Vue 3 (Options API), it was written as $watch inside the component object. In modern Vue 3 (2026 standard), especially with <script setup>, we use the imported watch() function — which is much cleaner, more flexible, and type-safe.
1. The Two Worlds: Options API vs Composition API
Old style (Options API – still in many legacy projects)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
<script> export default { data() { return { count: 0, user: { name: 'Rahul', age: 28 } } }, watch: { // Watch a data property count(newVal, oldVal) { console.log(`Count changed from ${oldVal} to ${newVal}`) }, // Deep watch nested object user: { handler(newUser) { console.log('User changed:', newUser) }, deep: true }, // Watch a computed property fullName(newName) { console.log('Full name is now:', newName) } }, computed: { fullName() { return `${this.user.name} (${this.user.age})` } } } </script> |
Modern style (<script setup> – what you should use in 2026)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
<script setup> import { ref, reactive, watch, computed } from 'vue' const count = ref(0) const user = reactive({ name: 'Rahul', age: 28 }) const fullName = computed(() => `${user.name} (${user.age})`) // Watch a ref watch(count, (newVal, oldVal) => { console.log(`Count changed from ${oldVal} to ${newVal}`) }) // Watch a reactive object deeply watch(user, (newUser, oldUser) => { console.log('User changed:', newUser) }, { deep: true }) // Watch a computed watch(fullName, (newName) => { console.log('Full name is now:', newName) }) // Watch multiple sources at once watch([count, () => user.age], ([newCount, newAge], [oldCount, oldAge]) => { console.log(`Count: ${oldCount} → ${newCount}, Age: ${oldAge} → ${newAge}`) }) </script> |
2. The Anatomy of watch() (Modern Composition API)
watch() has three main forms:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 1. Watch a single source (ref / reactive / getter / computed) watch(source, callback, options?) // 2. Watch multiple sources at once watch([source1, source2, ...], callback, options?) // 3. Watch an immediate getter function (most flexible) watch(() => someExpression, callback, options?) |
Callback arguments
|
0 1 2 3 4 5 6 7 8 9 |
watch(count, (newValue, oldValue, onCleanup) => { // newValue, oldValue are passed automatically // onCleanup lets you register cleanup code }) |
Most useful options (third argument)
| Option | Type | Default | What it does / When to use |
|---|---|---|---|
| immediate | boolean | false | Run callback immediately on setup (great for initial fetch) |
| deep | boolean | false | Watch nested changes in objects/arrays (expensive – use wisely) |
| flush | ‘pre’ / ‘post’ / ‘sync’ | ‘pre’ | When callback runs: before DOM (‘pre’), after DOM (‘post’), synchronously (‘sync’) |
| once | boolean | false | Run callback only once |
| onCleanup | function | — | Register cleanup function (cancel timers, subscriptions…) |
3. Real, Practical Examples (copy-paste ready)
Example 1 – Search input with debounced API call (very common pattern)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
<template> <div> <input v-model="searchQuery" placeholder="Search products..." /> <p v-if="loading">Searching...</p> <ul v-else> <li v-for="product in results" :key="product.id"> {{ product.name }} </li> </ul> </div> </template> <script setup lang="ts"> import { ref, watch } from 'vue' const searchQuery = ref('') const results = ref([]) const loading = ref(false) let timeout: ReturnType<typeof setTimeout> | null = null watch(searchQuery, (newQuery) => { // Debounce: wait 400ms after last keystroke if (timeout) clearTimeout(timeout) timeout = setTimeout(async () => { if (newQuery.length < 2) { results.value = [] return } loading.value = true try { // Fake API await new Promise(r => setTimeout(r, 800)) results.value = [ { id: 1, name: `${newQuery} Phone` }, { id: 2, name: `${newQuery} Laptop` } ] } finally { loading.value = false } }, 400) }) </script> |
Example 2 – Watch deep object + cleanup
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
<script setup> import { reactive, watch } from 'vue' const state = reactive({ user: { name: 'Rahul', age: 28 } }) let interval: ReturnType<typeof setInterval> | null = null watch(() => state.user, (newUser) => { console.log('User changed:', newUser) // Cleanup previous interval if (interval) clearInterval(interval) // Start new polling when user changes interval = setInterval(() => { console.log('Polling for user:', newUser.name) }, 5000) }, { deep: true }) // Important: cleanup when component unmounts onBeforeUnmount(() => { if (interval) clearInterval(interval) }) </script> |
Quick Reference Table – watch() in 2026
| What you want to watch | Syntax (Composition API) | Options you usually need |
|---|---|---|
| Single ref | watch(count, callback) | — |
| Reactive object (deep) | watch(user, callback, { deep: true }) | deep: true |
| Computed property | watch(fullName, callback) | — |
| Getter expression | watch(() => user.age * 2, callback) | — |
| Multiple sources | watch([count, user.age], callback) | — |
| Run immediately + deep | watch(source, callback, { immediate: true, deep: true }) | immediate, deep |
| Cleanup (cancel fetch, timer…) | watch(source, (newV, oldV, onCleanup) => { onCleanup(() => {…}) }) | onCleanup |
Pro Tips from Real Projects (Hyderabad 2026)
- Prefer getter function (watch(() => user.age, …)) over deep watching whole object — much more performant
- Always clean up in onCleanup or onBeforeUnmount — avoid memory leaks (timers, event listeners, AbortController…)
- Use flush: ‘post’ when you need DOM to be updated before your callback
- For debouncing/throttling → wrap in setTimeout or use lodash.debounce inside watch
- For search / filter inputs → watch + debounce is the classic pattern
- Don’t overuse deep watch — it can be expensive on large objects
Your mini practice task:
- Create search input → watch it with 400ms debounce → fake API call → show results
- Add a deep watch on a user object → log changes
- Add cleanup for a timer that starts when user changes
- Try immediate: true → see callback runs on mount
Any part confusing? Want full examples for:
- Watch + AbortController for cancelling fetch?
- Watch multiple sources with different flush modes?
- Watch vs watchEffect comparison?
- Real search + pagination with watch?
Just tell me — we’ll build the next reactive watcher together 🚀
