Vue watchers let you run custom code when reactive state changes. While computed properties calculate new values, watchers exist for side effects such as fetching API data, updating localStorage, or syncing browser titles.
Vue 3 gives you two watcher functions inside <script setup>: watch() and watchEffect(). Choosing the right one helps you control when side effects run and avoid extra network calls.
flowchart TD
Source["Reactive Ref or Getter"] --> Watcher["watch() or watchEffect()"]
Watcher --> Effect["Async Fetch, Storage Sync, or DOM Effect"]
watch() vs watchEffect()watch(source, callback): Watches explicit sources only. It runs lazily by default and gives you both the new value and the previous value.watchEffect(callback): Runs right away during setup and tracks every reactive ref read inside its body automatically.Here, watch() reacts to searchTopic changes with { immediate: true } and onCleanup to cancel stale timers. Meanwhile, watchEffect() keeps document.title synced with count.
<script setup>
import { ref, watch, watchEffect } from 'vue';
const searchTopic = ref('vue');
const results = ref([]);
const isLoading = ref(false);
const count = ref(0);
watch(
searchTopic,
(newTopic, oldTopic, onCleanup) => {
const query = newTopic.trim();
if (!query) {
results.value = [];
return;
}
isLoading.value = true;
const timer = setTimeout(() => {
results.value = [`Guide: ${query}`, `Examples: ${query}`];
isLoading.value = false;
}, 300);
onCleanup(() => clearTimeout(timer));
},
{ immediate: true }
);
watchEffect(() => {
document.title = `Clicks: ${count.value}`;
});
</script>
<template>
<div class="search-box">
<input v-model="searchTopic" placeholder="Search topic..." />
<p v-if="isLoading">Loading results...</p>
<ul v-else>
<li v-for="item in results" :key="item">{{ item }}</li>
</ul>
<button @click="count++">Increment Title Count</button>
</div>
</template>
watch(state.count, cb) passes a raw number, not a reactive source. Wrap object properties in a getter function: watch(() => state.count, cb).watch callback, or it will trigger itself endlessly.AbortController signal inside onCleanup.{ deep: true } in watch()?Pass { deep: true } when watching a ref holding a nested object and you want to react to nested property mutations. Note that watching a reactive() object directly enables deep tracking automatically.
{ immediate: true } do?Normally watch() waits until the source changes for the first time. Adding { immediate: true } runs the callback once right away with the initial value.
By default, watchers run before Vue updates the DOM. Pass { flush: 'post' } or use watchPostEffect() when your callback needs to measure updated DOM elements.
Sign in to track your learning journey, earn industry-recognized certificates, and join our elite developer community.
Quick Access With
You've completed this section! Take a quick 5-question quiz to check your understanding.