Vue 3 Reactivity: computed() vs watch() vs watchEffect() — When to Use Which?

computed vs watch vs watchEffect in Vue 3

Vue 3 Reactivity Battle: computed vs watch vs watchEffect

Start the clock!

⏱️ Time to Read: 6 minutes

🎯 What You’ll Achieve

By the end of this article, you will:

  • Stop guessing which reactive primitive to use.
  • Understand the "Mental Model" behind Vue's reactivity system.
  • Write cleaner, more performant code by avoiding common pitfalls.
  • Master real-world scenarios for computed, watch, and watchEffect.

If you've spent any time diving into the Vue.js ecosystem, specifically the Composition API, you've likely hit that wall. You know the one. You're staring at your IDE, cursor blinking, asking yourself:

"Wait... should this be a computed property? Or maybe a watch? But what about that cool watchEffect thing I saw on Twitter?" 🤔

It’s a rite of passage! They all react to data changes, so it’s easy to feel like they’re interchangeable. Spoiler alert: They aren't. Choosing the wrong one can lead to buggy code, infinite loops, or performance that crawls like a snail in molasses. 🐌

But don't worry! I’ve got you covered. We're going to break this down simply, logically, and with enough emoji to keep things fun. Let's dive in! 🌊


⚡ The Quick Cheat Sheet

In a rush? Here’s the "save-this-for-later" comparison table:

Featurecomputed() 🧠watch() 🕵️‍♂️watchEffect() 🎬
Primary JobDerive new dataReact to specfic changesRun side effects automatically
Returns Value?✅ Yes (Read-only ref)❌ No❌ No
Auto-Dependency?✅ Yes (Magic!)❌ No (Explicit)✅ Yes (Auto-magic)
Runs Immediately?✅ Yes (Lazy evaluation)❌ No (Lazy by default)✅ Yes (Eager)
Old Value Access?❌ No✅ Yes❌ No

🧠 The "Mental Model"

Before we look at code, let's set your brain's default mode for these tools:

  1. computed(): “I have ingredients, bake me a cake.” 🎂 (Input → Output)
  2. watch(): “If this specific door opens, bark at the mailman.” 🐕 (Event → Action)
  3. watchEffect(): “Keep your eyes open and do this whenever anything relevant changes.” 👀 (State → Effect)

1. computed() — The Smart Cacher 🧠

Use computed() when you want to create a new value derived from existing data. Think of it like a math formula in a spreadsheet.

It is a reactive getter. It looks at A and B to give you C.

Why it's awesome:

  • It's Cached! 💾 This is its superpower. A computed property only recalculates when its dependencies change. If you access it 1,000 times but the data hasn't changed, it runs the function once and returns the saved result 999 times.
  • It's Synchronous. It returns a value right now. (No async/await here, folks!)

Example: The Shopping Cart 🛒

import { ref, computed } from "vue";

const price = ref(100);
const quantity = ref(3);

// ✅ GOOD: Deriving a value
const total = computed(() => {
  return price.value * quantity.value;
});

console.log(total.value); // 300

price.value = 200; // Update the price
console.log(total.value); // 600 — Updates automatically! ✨

When to use computed()?

  • Filtering a list (e.g., specific search results).
  • Formating data (e.g., firstName + lastName = fullName).
  • Boolean logic (e.g., isFormValid).

🔥 Hot Tip: If you aren't using the return value, you should NOT be using computed().


2. watch() — The Specific Detective 🕵️‍♂️

watch() is for when you need to run a side effect (like an API call, DOM change, or logging) because a specific piece of data changed.

Unlike computed, it doesn't return a value. It just does stuff.

Why it's awesome:

  • Old vs. New: It gives you the oldValue and the newValue. Perfect for comparisons!
  • Lazy: It won't run until the data actually changes (unless you force it with { immediate: true }).
  • Precision: You tell Vue exactly what to watch. No surprises.

Example: The Search Bar 🔍

import { ref, watch } from "vue";

const searchQuery = ref("");

watch(searchQuery, (newQuery, oldQuery) => {
  console.log(`User changed search from "${oldQuery}" to "${newQuery}"`);

  // 🚀 Perform a side effect (like Fetching Data)
  performSearchAPI(newQuery);
});

⚠️ Common Gotcha: Watching Objects

If you try to watch a property of a reactive object directly, Vue might get confused. Use a getter function!

const user = reactive({ id: 1, name: "Alice" });

// ❌ WRONG: Passing number directly
// watch(user.id, (val) => ...)

// ✅ RIGHT: Use a getter
watch(
  () => user.id,
  (newId) => {
    console.log(`User ID switched to ${newId}`);
  },
);

3. watchEffect() — The Eager Observer 🎬

watchEffect() is the new kid on the block. It’s a bit of a hybrid. It runs a function immediately, tracks whatever reactive properties are used inside it, and re-runs whenever they change.

You don't tell it what to watch. It just knows. 🔮

Why it's awesome:

  • Less Boilerplate: You don't have to list every single dependency manually.
  • Immediate: It runs right away, making it great for setting up initial state that needs to stay in sync.

Example: Keeping Title in Sync 📑

import { ref, watchEffect } from "vue";

const articleId = ref(123);
const isPublished = ref(false);

watchEffect(() => {
  // Vue sees we used 'articleId' and 'isPublished' here.
  // It automatically tracks them! 🤯
  console.log(
    `Article ${articleId.value} is ${isPublished.value ? "Live" : "Hidden"}`,
  );

  // Maybe update the document title?
  document.title = `Article #${articleId.value}`;
});

💡 The "Cleanup" Function

Since watchEffect is often used for things like API calls or event listeners, you might need to clean up the previous run before the next one starts (to avoid race conditions).

watchEffect((onCleanup) => {
  const controller = new AbortController();

  fetch(`/api/data/${id.value}`, { signal: controller.signal })
    .then((data) => data.json())
    .then((res) => (results.value = res));

  // 🧹 Clean up! specific to the *previous* run
  onCleanup(() => {
    controller.abort(); // Cancel the old request if id changes!
  });
});

🥊 The Showdown: Which One Should I Use?

Let's simplify your decision-making process. Ask yourself:

  1. "Do I just need a new value based on other values?"
    • 👉 Use computed(). (Always try this first!)
  2. "Do I need to check the OLD value vs the NEW value?"
    • 👉 Use watch().
  3. "Do I need to trigger an API call or side effect explicitly when X changes?"
    • 👉 Use watch().
  4. "Do I need to run something immediately and keep it synced with multiple variables without listing them all?"
    • 👉 Use watchEffect().

🚫 Common Mistakes to Avoid

  • Using watch to calculate values.
    • Bad: Watching firstName and lastName to update a fullName ref.
    • Good: Just use a computed for fullName. It's cleaner and cache-friendly.
  • Async inside computed.
    • Bad: computed(async () => await fetch(...)) -> This returns a Promise, not your value!
    • Good: Use watch or watchEffect for async logic checking, and update a ref with the result.
  • Overusing watchEffect.
    • Because it tracks everything, sometimes it tracks too much. If your effect is re-running when you don't expect it to, switch to watch to be more explicit.

Conclusion 🎓

Vue 3's reactivity system is powerful, but power requires control.

  • computed is your architect — it builds data. 🏗️
  • watch is your guard dog — it waits for specific triggers. 🐕
  • watchEffect is your manager — it keeps the whole system moving. 👔

Mastering these three tools is a huge step toward becoming a Vue.js expert. Your code will be cleaner, faster, and much easier for your team (and your future self) to read.

Now, go forth and build something amazing! 🚀


Did this clarify the Reactivity mystery? Check out the official Vue.js Composition API Docs or dive into state management with Pinia for even more power!

Related posts