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.

Leave a Comment

Your email address will not be published. Required fields are marked *