100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
JavaScript

Watchers Explained

How watch() and watchEffect() let you run side effects in response to reactive state changes, and how to choose between them.

Reactivity FundamentalsIntermediate10 min readJul 9, 2026
Analogies

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.

vue
<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

Was this page helpful?

Topics covered

#JavaScript#VueJsStudyNotes#WebDevelopment#WatchersExplained#Watchers#Explained#Watch#Explicit#StudyNotes#SkillVeris