Chapter 90: Vue ‘watch’ Option
The watch option
This is the original, classic way to react to changes in reactive data (props, data, computed properties, etc.). Even in 2026 — when almost every new project uses <script setup> + watch() from Composition API — you still need to understand the old watch: { … } syntax because:
- You will read/maintain legacy Vue 2 / early Vue 3 code
- Many job interviews (especially in India) still ask about Options API
- Many older tutorials, Stack Overflow answers, and plugins still show it
- Vue Devtools still displays watchers in this style
So let’s go through it step by step — like I’m sitting next to you explaining both the old world and why we mostly moved to the new one.
1. What is the watch option? (Very simple mental model)
watch is an object where:
- each key = what you want to observe (a property name, a dot-notation path, or a function)
- each value = either a function (the callback) or an object with handler, deep, immediate, etc.
Vue automatically calls your callback whenever the watched value changes.
|
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 |
watch: { // Simple property watch count(newVal, oldVal) { console.log(`Count changed from ${oldVal} to ${newVal}`) }, // Deep watch on nested object user: { handler(newUser, oldUser) { console.log('User changed:', newUser) }, deep: true }, // Immediate + deep searchQuery: { handler(newQuery) { this.fetchResults(newQuery) }, immediate: true, deep: true }, // Dot notation for nested property 'user.name'(newName, oldName) { console.log(`Name changed from ${oldName} to ${newName}`) } } |
2. Real, Complete Example – Search + Counter + User Form
|
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 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
<template> <div class="demo"> <h2>Options API – watch example</h2> <!-- Search – triggers API-like call --> <input v-model.trim="searchQuery" placeholder="Search products..." /> <p v-if="loading">Searching...</p> <ul v-else> <li v-for="product in searchResults" :key="product.id"> {{ product.name }} </li> </ul> <!-- Counter – simple watch --> <p>Count: {{ count }}</p> <button @click="count++">+1</button> <!-- User form – deep watch --> <div class="user-form"> <label>Name: <input v-model="user.name" /></label> <label>Age: <input v-model.number="user.age" type="number" /></label> <p>Current user: {{ user.name }}, {{ user.age }} years old</p> </div> </div> </template> <script> export default { name: 'WatchDemo', data() { return { searchQuery: '', loading: false, searchResults: [], count: 0, user: { name: 'Rahul', age: 28 } } }, // ── This is the watch option ─────────────────────────────── watch: { // 1. Simple watch – reacts to every change count(newVal, oldVal) { console.log(`Count changed from ${oldVal} to ${newVal}`) if (newVal >= 10) { alert('You reached 10 clicks!') } }, // 2. Debounced search (very common real pattern) searchQuery: { handler(newQuery) { // Clear previous timer if (this.searchTimer) clearTimeout(this.searchTimer) this.searchTimer = setTimeout(() => { this.performSearch(newQuery) }, 400) // 400ms debounce }, immediate: false }, // 3. Deep watch on object user: { handler(newUser, oldUser) { console.log('User object changed:', newUser) }, deep: true }, // 4. Watch nested property with dot notation 'user.age'(newAge, oldAge) { console.log(`Age changed from ${oldAge} to ${newAge}`) if (newAge >= 30) { alert('You are now officially 30+!') } } }, methods: { async performSearch(query) { if (!query.trim()) { this.searchResults = [] return } this.loading = true try { // Fake API delay await new Promise(r => setTimeout(r, 800)) this.searchResults = [ { id: 1, name: `${query} Phone` }, { id: 2, name: `${query} Laptop` }, { id: 3, name: `${query} Watch` } ] } finally { this.loading = false } } } } </script> <style scoped> .demo { max-width: 600px; margin: 3rem auto; padding: 2rem; background: white; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.08); } .user-form { margin-top: 2rem; padding: 1.5rem; background: #f8fafc; border-radius: 8px; } .user-form label { display: block; margin: 1rem 0; } input { padding: 0.8rem; width: 100%; max-width: 300px; border: 1px solid #d1d5db; border-radius: 6px; } </style> |
4. Important Rules & Gotchas (2026 Must-Know)
| Rule / Gotcha | Correct Behavior / Best Practice |
|---|---|
| watch is an object | watch: { count() { … } } — never an array or function |
| Simple property watch | count(newVal, oldVal) { … } — gets new & old value |
| Deep watch | user: { handler() { … }, deep: true } — watches nested changes |
| Immediate watch | searchQuery: { handler() { … }, immediate: true } — runs once on create |
| Dot notation for nested props | ‘user.age'(newAge) { … } — cleaner than deep watch for single property |
| Avoid arrow functions | count: () => {} → this becomes undefined → bug |
| Still used in 2026? | Yes — in legacy code, some plugins, interviews, but not in new <script setup> code |
5. Modern Vue 3 – Why We Almost Never Use watch: { … } Anymore
In <script setup> (2026 standard):
|
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 |
<script setup> import { ref, watch } from 'vue' const count = ref(0) const user = reactive({ name: 'Rahul', age: 28 }) const searchQuery = ref('') // Simple watch watch(count, (newVal, oldVal) => { console.log(`Count: ${oldVal} → ${newVal}`) }) // Deep watch watch(user, (newUser) => { console.log('User changed:', newUser) }, { deep: true }) // Immediate + deep watch(searchQuery, (newQuery) => { // fetch... }, { immediate: true }) </script> |
→ No watch object → No this → Just imported watch() function → Much more flexible (watch getters, arrays, multiple sources, onCleanup, etc.)
Quick Summary Table – watch in 2026
| Question | Options API (legacy) | Composition API (<script setup>) | What you should do in new code |
|---|---|---|---|
| How to define watcher? | watch: { count() { … } } | watch(count, callback) | Composition API |
| Access in callback | newVal, oldVal | newVal, oldVal | — |
| Deep watch | deep: true | { deep: true } | — |
| Immediate watch | immediate: true | { immediate: true } | — |
| Still used in 2026? | Yes — legacy code, plugins, interviews | Almost never (except legacy) | Avoid unless maintaining old code |
Final 2026 Advice from Real Projects
- In new projects → never write watch: { … } — use watch() from Composition API
- When you see watch as an object → it is Options API (legacy style)
- Learn to read Options API — many jobs, open-source projects, tutorials still use it
- Never teach beginners the watch option as primary — start with watch() in <script setup>
- If migrating old code → gradually convert watch: { … } → watch() calls
Your mini homework:
- Create the search + counter component above in Options API
- Type in search → see debounced watcher work
- Change user.age → see deep watch trigger
- Convert it to <script setup> → compare readability & flexibility
Any part confusing? Want to see:
- Full Options API vs Composition API watcher comparison project?
- How watch looks in Vue Devtools?
- Common bugs with deep watchers & performance?
- Real component written in both styles?
Just tell me — we’ll convert and compare together step by step 🚀
