Vue.js Watchers
Watchers let you run code automatically whenever a specific piece of data changes. While computed properties calculate and return a new value, watchers perform actions — like calling an API, saving to localStorage, or showing a notification — in response to a change.
When to Use a Watcher vs a Computed Property
Diagram: Watcher vs Computed — Purpose Comparison
Computed Property: data changes → calculate a new value → return it Example: fullName = firstName + " " + lastName Purpose: produce a value Watcher: data changes → run a side effect → no return value needed Example: username changes → call API to check availability Purpose: trigger an action
Basic Watcher — The watch Option
Define watchers in the watch option. The key is the name of the data property to watch. The value is a function that receives the new value and the old value.
<div id="app">
<input v-model="city" placeholder="Enter city name">
<p>{{ weatherInfo }}</p>
</div>
<script>
Vue.createApp({
data() {
return {
city: "",
weatherInfo: ""
};
},
watch: {
city(newValue, oldValue) {
if (newValue.length > 2) {
this.weatherInfo = "Loading weather for " + newValue + "...";
// In a real app, you would fetch weather data here
} else {
this.weatherInfo = "";
}
}
}
}).mount("#app");
</script>
Diagram: Watcher Trigger Flow
User types "London" into the input
│
▼
city data changes to "London"
│
▼
Vue calls watch.city(newValue="London", oldValue="Londo")
│
▼
Function runs:
"London".length > 2 → true
weatherInfo = "Loading weather for London..."
│
▼
Template updates: shows the weatherInfo message
The immediate Option — Run on Load
By default, a watcher does not run when the component first loads — it only runs when the data changes. Use immediate: true to run the watcher right away on component creation.
watch: {
userId: {
handler(newId) {
// Fetch user profile for this ID
this.loadUserProfile(newId);
},
immediate: true // runs once on component load, then on every change
}
}
Diagram: immediate Watcher Timing
Without immediate: Component loads → watcher does NOT run userId changes → watcher runs With immediate: true: Component loads → watcher runs immediately (handler called) userId changes → watcher runs again
The deep Option — Watching Nested Data
Vue does not detect changes inside nested objects by default — only changes to the object reference itself trigger the watcher. Use deep: true to watch all nested properties.
data() {
return {
settings: {
theme: "light",
fontSize: 16,
notifications: true
}
};
},
watch: {
settings: {
handler(newSettings) {
localStorage.setItem("settings", JSON.stringify(newSettings));
console.log("Settings saved.");
},
deep: true // watches settings.theme, settings.fontSize, etc.
}
}
Diagram: Deep Watch vs Shallow Watch
Without deep:
settings.theme = "dark" → watcher does NOT fire
settings = { ... } → watcher fires (reference changed)
With deep: true:
settings.theme = "dark" → watcher fires ✓
settings.fontSize = 18 → watcher fires ✓
settings = { ... } → watcher fires ✓
Watching a Nested Property Directly
To watch a single nested property without deep: true, use a string key with dot notation.
watch: {
"user.email"(newEmail, oldEmail) {
console.log("Email changed from", oldEmail, "to", newEmail);
// Send verification email
}
}
This is more efficient than deep: true because Vue only watches the specific path instead of the whole object tree.
Watching Multiple Sources
Each data property gets its own watcher. You can define as many as needed.
watch: {
searchQuery(newQuery) {
this.runSearch(newQuery);
},
selectedCategory(newCat) {
this.filterResults(newCat);
},
pageNumber(newPage) {
this.loadPage(newPage);
}
}
Watchers in the Composition API — watch()
In the Composition API, use the imported watch() function inside setup() or <script setup>.
<script setup>
import { ref, watch } from "vue";
const username = ref("");
const isAvailable = ref(null);
watch(username, async (newName) => {
if (newName.length < 3) {
isAvailable.value = null;
return;
}
// Simulate checking username availability
isAvailable.value = newName !== "admin";
});
</script>
<template>
<input v-model="username" placeholder="Choose a username">
<p v-if="isAvailable === true" style="color:green">✓ Available</p>
<p v-if="isAvailable === false" style="color:red">✗ Username taken</p>
</template>
watchEffect() — Automatic Dependency Tracking
watchEffect() runs a function immediately and re-runs it whenever any reactive value it reads changes. You do not specify what to watch — Vue figures it out automatically.
<script setup>
import { ref, watchEffect } from "vue";
const page = ref(1);
const filter = ref("all");
watchEffect(() => {
// Runs now AND every time page or filter changes
console.log("Fetching: page", page.value, "filter", filter.value);
// fetch("/api/items?page=" + page.value + "&filter=" + filter.value)
});
</script>
Diagram: watch() vs watchEffect()
watch(source, callback): ✓ You specify exactly what to watch ✓ Receives old and new values ✓ Does not run immediately by default Best for: responding to a specific value change watchEffect(callback): ✓ Automatically tracks all reactive values read inside it ✗ No access to old value ✓ Runs immediately on first call Best for: syncing multiple reactive values at once
Stopping a Watcher
In the Composition API, watch() and watchEffect() return a stop function. Call it to stop watching.
<script setup>
import { ref, watch } from "vue";
const count = ref(0);
const stopWatcher = watch(count, (val) => {
console.log("Count is now:", val);
if (val >= 5) {
stopWatcher(); // stop watching after count reaches 5
console.log("Watcher stopped.");
}
});
</script>
Diagram: Self-Stopping Watcher
count = 1 → logs "Count is now: 1" → keeps watching count = 2 → logs "Count is now: 2" → keeps watching count = 5 → logs "Count is now: 5" → stopWatcher() called count = 6 → watcher no longer fires count = 7 → watcher no longer fires
Practical Example: Auto-Save Form
<div id="app">
<textarea v-model="note" placeholder="Write your note..."></textarea>
<p>{{ saveStatus }}</p>
</div>
<script>
Vue.createApp({
data() {
return {
note: "",
saveStatus: "No changes yet.",
saveTimer: null
};
},
watch: {
note(newText) {
this.saveStatus = "Unsaved changes...";
clearTimeout(this.saveTimer);
this.saveTimer = setTimeout(() => {
localStorage.setItem("draft", newText);
this.saveStatus = "Saved at " + new Date().toLocaleTimeString();
}, 1000);
}
}
}).mount("#app");
</script>
Diagram: Debounced Auto-Save
User types... Each keystroke → watcher fires → resets 1s timer → shows "Unsaved..." User stops typing for 1 second Timer fires → saves to localStorage → shows "Saved at 10:45:32" Without debounce: saves on every single keystroke (wasteful) With debounce: saves only after typing pauses (efficient)
Summary
Watchers run code in response to data changes. Define them in the watch option (Options API) or with the watch() / watchEffect() functions (Composition API). Use immediate: true to run the handler once on load. Use deep: true to detect changes inside nested objects. Watchers are for side effects — API calls, saving data, logging — not for calculating values. For derived values, use computed properties instead.
