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



(2) Pain Point: A request is sent with every keystroke, causing the API to crash

Alice implemented a product search box in the admin:

JS
<<<<<<< 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:

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

JS
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:



3. Basic Syntax of watch()

(1) 4 parameters

JS
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

JS
const count = ref(0)

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

(3) Monitoring Multiple refs

JS
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

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

(2) Listening to reactive objects

JS
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

JS
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

JS
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

JS
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

JS
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

JS
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

JS
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

JS
// 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:

TEXT 📖 Display only
Vue component renders the described UI in the browser.
VUE
<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:

TEXT 📖 Display only
Two-way data binding on form inputs via v-model.
Displays: count
Visible text: Count: {{ count }}

Output:

TEXT 📖 Display only
Form with v-model bound to: count.
Displays: count
VUE
<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:

TEXT 📖 Display only
Renders: Two-way data-bound input field syncing with component state.

▶ Example: 3. Listening for changes in reactive depth

Output:

TEXT 📖 Display only
Renders: Two-way data-bound input field syncing with component state.
JS
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:

TEXT 📖 Display only
'User changed:', JSON.stringify(newVal

▶ Example: 4. watchEffect Automatic Tracking

Output:

TEXT 📖 Display only
'User changed:', JSON.stringify(newVal
JS
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:

TEXT 📖 Display only
count: [count.value], name: [name.value]

▶ Example: 5. Implementing Debounce and Throttling

Output:

TEXT 📖 Display only
count: [count.value], name: [name.value]
JS
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:

TEXT 📖 Display only
'Search:', val
'Scroll:', val

▶ Example: 6. Quick Reference for 5 Common Mistakes

Output:

TEXT 📖 Display only
'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

Q Should I use watch or watchEffect?
A Use 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).
Q When watching a ref, what are the types of the old and new values?
A .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.
Q When using watch to observe a ref wrapper object, how do you observe its internal properties?
A There are three ways: (1) watch(ref, fn, { deep: true }); (2) watch(() => ref.value.x, fn) (getter); (3) Switch to a reactive wrapper.
Q Can watch monitor props?
A Yes. watch(() => props.count, fn). Note that props are read-only; do not modify them in the watch callback.
Q What is oldVal when immediate: true?
A 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.
Q Can watch be asynchronous?
A The callback function can be async, but watch itself is triggered synchronously. Place the asynchronous logic inside the callback: (newVal) => { setTimeout(() => {...}, 0) }.

📖 Summary


📝 Exercises

  1. Basic Questions (Difficulty: ⭐)

    Use watch to monitor a ref variable count:

    • When count changes, print "count: ${old} → ${new}"
    • Use immediate: true to execute the callback immediately once
  2. 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)
  3. Challenge Problem (Difficulty: ⭐⭐⭐)

    Implement a complete search, autocomplete, and history system:

    1. Anti-shake search (300 ms)
    2. Display the list of search results (v-for)
    3. Save the last 5 search entries
    4. Click a history item to search again
    5. Cancel pending requests when switching search terms
    6. Implement using watchEffect and 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%

🙏 帮我们做得更好

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

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