Watchers Explained
While computed properties derive a new reactive value from existing state, watchers exist to run imperative side effects in response to reactive state changing — things like logging, making an API call, updating a non-Vue-managed DOM element, or synchronizing with localStorage. Vue's Composition API provides two watcher functions: watch(), which explicitly tracks specified source(s) and gives you access to both the new and old values, and watchEffect(), which automatically tracks whatever reactive dependencies are read inside its callback and re-runs whenever any of them change.
Cricket analogy: A computed run rate is like a scoreboard deriving a new number from existing data, while a watcher is like a groundskeeper who reacts to rain starting by covering the pitch — an imperative side effect triggered by a change, not a derived value.
watch(): Explicit Source Tracking
watch() takes a source (a ref, a getter function returning a value, or an array of sources) and a callback that receives the new and old values. Crucially, watch() is lazy by default — the callback does not run immediately when the watcher is created, only after the source changes for the first time — unless you pass { immediate: true }. This explicitness makes watch() ideal when you need the old value for comparison, or when you want precise control over exactly which piece of state triggers the effect, rather than any reactive value touched anywhere in the callback.
Cricket analogy: watch() is like a specific fielder assigned to track only the striker's bat, staying idle until the ball is actually hit — it does nothing at the start of the over unless told '{ immediate: true }', and it can compare the current shot to the previous one.
watchEffect(): Automatic Dependency Tracking
watchEffect() runs its callback immediately once on creation, and during that run it automatically tracks every reactive value the callback reads; on any subsequent change to those tracked values, the callback re-runs (and re-tracks dependencies, since a different code path might read different reactive values next time). This is convenient when an effect naturally depends on multiple reactive values and you don't need the previous value — but its implicit dependency tracking can make it harder to see at a glance exactly what will trigger the effect, especially in larger callbacks with conditional logic.
Cricket analogy: watchEffect() is like an umpire who immediately surveys the whole field on taking position and then reacts automatically to whatever changes — a fielder moving, a bail falling — but with so many things being watched at once, spotting exactly what triggered a decision review can get tricky.
<script setup>
import { ref, watch, watchEffect } from 'vue'
const searchQuery = ref('')
const results = ref([])
// watch: explicit source, access to old value, debounced-style side effect
watch(searchQuery, async (newQuery, oldQuery) => {
if (!newQuery.trim()) {
results.value = []
return
}
const response = await fetch(`/api/search?q=${encodeURIComponent(newQuery)}`)
results.value = await response.json()
})
// watchEffect: runs immediately, auto-tracks searchQuery.value
watchEffect(() => {
document.title = searchQuery.value
? `Results for "${searchQuery.value}"`
: 'Search'
})
</script>To deeply watch a reactive() object or a ref containing a nested object for changes to any nested property, pass { deep: true } to watch(). This trades off performance (Vue must recursively traverse and compare the object) for convenience, so use it deliberately rather than by default on large or deeply nested structures.
A common mistake is watching a reactive object's property directly as watch(state.count, callback) instead of a getter — this passes the current primitive value, not a reactive source, so the watcher never fires again. The fix is to pass a getter function: watch(() => state.count, callback).
- watch() explicitly tracks a given source and provides both new and old values in its callback.
- watch() is lazy by default; pass { immediate: true } to run the callback on creation too.
- watchEffect() runs immediately and auto-tracks every reactive value read during its callback.
- watchEffect() re-tracks dependencies on every run, which can change if the callback takes different code paths.
- Use { deep: true } with watch() to detect nested property changes inside objects, at a performance cost.
- Watching state.property directly (instead of a getter) silently breaks the watcher — always use () => state.property.
Practice what you learned
1. By default, does watch() run its callback immediately when created?
2. What does watchEffect() do differently from watch() in terms of dependency tracking?
3. Why does `watch(state.count, callback)` fail to react to future changes when `state` is a reactive() object?
4. What option must be passed to watch() to detect changes to nested properties within a watched object?
5. Which watcher API gives direct access to both the new and old value of the watched source?
Was this page helpful?
You May Also Like
Computed Properties
How Vue's computed() function derives cached, reactive values from other state, and why it's preferred over methods for derived data in templates.
reactive() and ref()
A deep dive into Vue 3's two core reactivity primitives — ref() for any value type and reactive() for objects — and when to use each.
Fetching Data in Vue
Learn common patterns for fetching data in Vue components and composables, including lifecycle timing, reactivity pitfalls, and race conditions.
Lifecycle Hooks in the Composition API
Learn how onMounted, onUpdated, onUnmounted and other lifecycle hook functions let you run code at specific points in a component's life.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebSockets Study Notes
Web Development · 30 topics
Web DevelopmentWebAssembly Study Notes
WebAssembly · 30 topics
Web DevelopmentgRPC Study Notes
Protocol Buffers · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics