Vue.js: Watchers: watch

Last updated: 2026-10-20

Watchers (watch / watchEffect) let you observe changes in reactive data and run side effects when those changes happen. computed derives new values; watch triggers actions (such as sending a request or setting state).

watch is a core part of Vue's reactivity system, with 3 main scenarios: send an API request when data changes, debounce form input, and sync data across sources.

Related Core:watchEffect() watch()

1. What You'll Learn


2. A Search Box API Performance Problem

(2) The Pain: A Request per Keystroke, and the API Collapses

Alice implemented a product search box in the admin:

JS
// ❌ The broken version: a request per character
const { ref } = Vue

const searchQuery = ref('')

watch(searchQuery, async (newValue) => {
  // Send a request on every keystroke
  const results = await fetch(`/api/search?q=${newValue}`)
  searchResults.value = await results.json()
})

The API server collapses:

Backend developer Charlie reports:

"Alice, your search is hammering the API. We need debouncing — wait until the user stops typing for 300ms, then send the request."

(2) The Vue watch + Debounce Solution

JS
const { ref, watch } = Vue

const searchQuery = ref('')
const searchResults = ref([])

// Debounce: only request if the value stays unchanged for 300ms
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()
  }, 300)
})

(3) The Payoff

After adding debouncing:


3. watch() Basics

(1) The 4 Parameters

JS
const { ref, watch } = Vue

const count = ref(0)

watch(
  // 1. Source: the reactive data to watch
  count,
  // 2. Callback: (new value, old value) => {...}
  (newVal, oldVal) => {
    console.log(`count: ${oldVal} → ${newVal}`)
  },
  // 3. Options (optional): immediate, deep, flush
  { immediate: true, deep: true }
)

(2) Watching a ref

JS
const count = ref(0)

// Watch a ref
watch(count, (newVal, oldVal) => {
  console.log(`count: ${oldVal} → ${newVal}`)
})

(3) Watching Multiple refs

JS
const firstName = ref('Alice')
const lastName = ref('Smith')

// Watch several sources
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
  console.log(`Name: ${newFirst} ${newLast}`)
})

▶ Example: Watching a ref with Basic watch

▶ Try it Yourself

Output:

TEXT
A number input starting at 0.
Changing it to 5 prints Count changed: 0 → 5 in the console and updates Count: 5 on the page.
TEXT
Count: 0
Check the console for the change log

console: Count changed: 0 → 5

4. The 4 Kinds of Watch Source

(1) Watching a ref

JS
const count = ref(0)
watch(count, (newVal, oldVal) => {
  console.log(`count: ${oldVal} → ${newVal}`)
})

(2) Watching a reactive object

JS
const { reactive, watch } = Vue

const user = reactive({ name: 'Alice', age: 25 })

// Watch the whole reactive object (deep automatically)
watch(user, (newVal, oldVal) => {
  console.log('User changed:', newVal)
})

// ✅ Mutating a nested property also triggers it
user.name = 'Bob'  // triggers

(3) Watching a getter function

JS
const state = reactive({ user: { profile: { name: 'Alice' } } })

// Watch only a specific property (precise control)
watch(
  () => state.user.profile.name,
  (newVal) => {
    console.log(`Name: ${newVal}`)
  }
)

(4) Watching computed

JS
const count = ref(0)
const doubleCount = computed(() => count.value * 2)

watch(doubleCount, (newVal) => {
  console.log(`Double: ${newVal}`)
})

(4) Comparing the 4 Sources

Source Syntax Deep? Good for
ref watch(ref, fn) Shallow by default A single ref
reactive watch(reactive, fn) Deep automatically Objects
getter watch(() => obj.x, fn) Shallow by default Specific properties
computed watch(computed, fn) Shallow by default Derived values

▶ Example: Watching Deep Changes in a reactive Object

▶ Try it Yourself

Output:

TEXT
Starts at Name: Alice, Age: 25, Theme: dark, Changes: 0.
Clicking Rename to Bob changes the name and Changes becomes 1;
clicking Switch to light changes the theme and Changes becomes 2.
TEXT
Name: Bob
Age: 25
Theme: light
Changes: 2

5. watchEffect(): Automatic Dependency Tracking

(1) Basic Syntax

JS
const { ref, watchEffect } = Vue

const count = ref(0)
const name = ref('Alice')

// No source to name: reactive data used in the body is tracked automatically
watchEffect(() => {
  console.log(`count: ${count.value}, name: ${name.value}`)
  // Reruns when count changes
  // Also reruns when name changes
})

(2) watchEffect vs watch

Dimension watch watchEffect
Source Specified manually Tracked automatically
Runs immediately No by default (needs immediate: true) ✅ Yes
Old and new values ✅ Both ❌ New value only
Side-effect cleanup onCleanup onCleanup
Best for Precise watching General side effects

(3) Cleaning Up in watchEffect

JS
const { watchEffect, onCleanup } = Vue

watchEffect((onCleanup) => {
  // Set up timers, subscriptions, event listeners
  const timer = setInterval(() => {
    console.log('tick')
  }, 1000)
  
  // Cleanup: called before the next run or when the component unmounts
  onCleanup(() => {
    clearInterval(timer)
  })
})

▶ Example: watchEffect Automatic Tracking

▶ Try it Yourself

Output:

TEXT
effect runs: 1 on load.
Each click on count++ or Rename to Bob reruns the effect, so the counter goes 2, 3, 4… while the page shows the new count and name.
TEXT
count: 1
name: Bob
effect runs: 3

6. Options: immediate / deep / flush

(1) immediate: Run Right Away

JS
const count = ref(0)

// Default: only fires when count changes
watch(count, (newVal) => console.log(newVal))  // does not run immediately

// immediate: true: fire once right away
watch(count, (newVal) => console.log(newVal), { immediate: true })
// ✅ Runs immediately: 0 → 0

(2) deep: Watch Nested Properties

JS
const { ref, watch } = Vue

const user = ref({ profile: { name: 'Alice' } })

// Default: only triggers when the ref's .value is replaced as a whole
watch(user, () => console.log('changed'))  // ❌ user.value.profile.name = 'Bob' does not trigger

// deep: true: nested property changes trigger too
watch(user, () => console.log('changed'), { deep: true })
// ✅ user.value.profile.name = 'Bob' triggers

(3) flush: When the Callback Runs

JS
// Default 'pre': fires before the component updates
watch(source, fn, { flush: 'pre' })

// 'post': fires after the component updates (the DOM is already updated)
watch(source, fn, { flush: 'post' })

// 'sync': fires synchronously (rarely used, poor performance)
watch(source, fn, { flush: 'sync' })

7. Complete Example: Search Box + Debounce

▶ Example: Debounced Search (CDN Simulation)

▶ Try it Yourself

Output:

TEXT
Typing "iphone" quickly shows Recent query: (empty) at first;
300ms after you stop typing it becomes Recent query: iphone and Triggers increments by 1 instead of 6.
TEXT
Recent query: iphone
Triggers: 1

Note: a real project would use fetch + a backend API; here setTimeout simulates the trigger logic.

(1) 📊 Quick Reference for the 5 Common Mistakes

Mistake Symptom Fix
Watching reactive without deep Nested changes do not trigger watch(obj, fn, { deep: true })
Watching the whole ref-wrapped object Nested changes do not trigger Switch to reactive or add deep
Writing an infinite loop in watchEffect The browser freezes Do not mutate data the body reads
Never clearing timers Memory leak Use onCleanup
Confusing immediate You expect an old value but get none Use immediate + oldVal (may be undefined)

Related Core:watchEffect() watch()

❓ FAQ

Q watch or watchEffect?
A Prefer watch when you can (precise control + old and new values). watchEffect fits "run something when this data changes, but I don't want to list every dependency". Performance is similar; watchEffect is slightly slower because it has to track the body's dependencies.
Q When watching a ref, what type are the old and new values?
A The .value. If the ref wraps an object, both values are the same ref.value reference unless the whole object is replaced. To watch nested properties, use reactive or a getter function.
Q How do I watch nested properties of a ref-wrapped object?
A Three ways: (1) watch(ref, fn, { deep: true }); (2) watch(() => ref.value.x, fn) (a getter); (3) wrap the data with reactive instead.
Q Can watch observe props?
A Yes: watch(() => props.count, fn). Remember that props are read-only, so never mutate them inside the callback.
Q What is oldVal when immediate: true?
A undefined (there is no "old value" on the first run). Read the previous value from the ref's initial value or from the store.
Q Can watch be asynchronous?
A The callback can be async, but watch itself fires synchronously. Put async logic in the callback: (newVal) => { setTimeout(() => {...}, 0) }.
Q When does onCleanup fire?
A Two moments: (1) before the watcher runs again (cleanup of the previous run); (2) when the component unmounts. It is the right place to clear timers, cancel requests, and remove event listeners.

📖 Summary


📝 Exercises

  1. Basic (⭐) Use watch to observe a ref called count:

    • Print "count: ${old} → ${new}" when count changes
    • Use immediate: true so the callback runs once right away
  2. Intermediate (⭐⭐) Build an "autosave" feature with watch:

    • Edit a ref text called content
    • After 1 second, call save(content) automatically (debounce)
    • Clear the timer when navigating away (component unmount)
  3. Challenge (⭐⭐⭐) Build a complete search + autocomplete + history system:

    1. Debounced search (300ms)
    2. Show the result list (v-for)
    3. Keep the 5 most recent searches
    4. Click a history item to search again
    5. Cancel pending requests when the query changes
    6. Implement it with watchEffect + onCleanup
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏