beginner Step 4 of 15

Computed Properties and Watchers

Vue.js Development

Computed Properties and Watchers

Computed properties and watchers are two complementary tools for responding to data changes in Vue. Computed properties are derived values that are cached and only recalculated when their dependencies change — they are the right choice for transforming or combining reactive data for display. Watchers observe specific data sources and execute side effects when they change — they are the right choice for asynchronous operations, API calls, or any action that goes beyond simply computing a value. Knowing when to use each is a key skill in Vue development.

Computed Properties

A computed property is defined as a function that returns a value derived from reactive data. Vue caches the result and only recalculates it when the reactive dependencies change. In templates, computed properties are used exactly like data properties.

<template>
  <div>
    <input v-model="firstName" placeholder="First name" />
    <input v-model="lastName" placeholder="Last name" />
    <p>Full name: {{ fullName }}</p>

    <input v-model="searchQuery" placeholder="Search..." />
    <ul>
      <li v-for="item in filteredItems" :key="item.id">
        {{ item.name }} - ${{ item.price.toFixed(2) }}
      </li>
    </ul>
    <p>{{ filteredItems.length }} of {{ items.length }} items shown</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      firstName: "John",
      lastName: "Doe",
      searchQuery: "",
      items: [
        { id: 1, name: "Laptop", price: 999.99 },
        { id: 2, name: "Mouse", price: 29.99 },
        { id: 3, name: "Keyboard", price: 79.99 },
        { id: 4, name: "Monitor", price: 449.99 },
        { id: 5, name: "Headphones", price: 149.99 },
      ],
    };
  },
  computed: {
    fullName() {
      return `${this.firstName} ${this.lastName}`;
    },
    filteredItems() {
      const query = this.searchQuery.toLowerCase();
      if (!query) return this.items;
      return this.items.filter((item) =>
        item.name.toLowerCase().includes(query)
      );
    },
    totalValue() {
      return this.items.reduce((sum, item) => sum + item.price, 0);
    },
  },
};
</script>

Writable Computed Properties

By default, computed properties are getter-only. You can provide both a getter and a setter when you need two-way binding with a computed value.

<script>
export default {
  data() {
    return {
      firstName: "John",
      lastName: "Doe",
    };
  },
  computed: {
    fullName: {
      get() {
        return `${this.firstName} ${this.lastName}`;
      },
      set(newValue) {
        const parts = newValue.split(" ");
        this.firstName = parts[0] || "";
        this.lastName = parts.slice(1).join(" ") || "";
      },
    },
  },
};
// Now you can: this.fullName = "Jane Smith"
// This sets firstName = "Jane" and lastName = "Smith"
</script>

Watchers

Watchers observe a reactive data source and call a callback whenever it changes. They are the right tool for performing side effects like API calls, logging, or complex operations that go beyond computing a value.

<template>
  <div>
    <input v-model="searchQuery" placeholder="Search users..." />
    <p v-if="isLoading">Searching...</p>
    <ul v-else>
      <li v-for="user in results" :key="user.id">{{ user.name }}</li>
    </ul>
  </div>
</template>

<script>
export default {
  data() {
    return {
      searchQuery: "",
      results: [],
      isLoading: false,
    };
  },
  watch: {
    // Simple watcher
    searchQuery(newValue, oldValue) {
      console.log(`Query changed from "${oldValue}" to "${newValue}"`);
      this.debouncedSearch();
    },

    // Deep watcher for nested objects
    // user: {
    //   handler(newValue) { console.log("User changed:", newValue); },
    //   deep: true,
    // },

    // Immediate watcher — runs on component creation
    // selectedCategory: {
    //   handler(newValue) { this.fetchProducts(newValue); },
    //   immediate: true,
    // },
  },
  methods: {
    debouncedSearch() {
      clearTimeout(this._searchTimer);
      this._searchTimer = setTimeout(async () => {
        if (!this.searchQuery.trim()) {
          this.results = [];
          return;
        }
        this.isLoading = true;
        try {
          const response = await fetch(
            `/api/users?q=${encodeURIComponent(this.searchQuery)}`
          );
          this.results = await response.json();
        } finally {
          this.isLoading = false;
        }
      }, 300);
    },
  },
};
</script>
Tip: Use computed properties when you need to derive a value from reactive data. Use watchers when you need to perform side effects (API calls, timers, DOM manipulation) in response to data changes. If you find yourself setting a data property inside a watcher, consider whether a computed property would be simpler.

Key Takeaways

  • Computed properties are cached, derived values that recalculate only when dependencies change.
  • Writable computed properties support both getter and setter functions for two-way binding.
  • Watchers observe data sources and run callbacks for side effects like API calls.
  • Use deep: true to watch nested object changes and immediate: true to run on creation.
  • Prefer computed properties over watchers when the goal is simply deriving a display value.