# Computed Properties and Watchers

Course: [Vue.js](https://www.devtutorials.ai/learn/vue) · Section: Reactivity
Source: https://www.devtutorials.ai/learn/vue/reactivity/computed-properties-and-watchers

> computed() and caching, computed vs functions, writable computed, watch() on refs, getters, reactive objects and many sources, deep, immediate and once, watchEffect() and flush timing, stopping a watcher, onWatcherCleanup() for stale requests, and when to use computed or a watcher.

## Derived Values and Side Effects

- Some values come from other values. A bill total comes from price and plates.
- **Derived value** = a value worked out from state. Vue gives you `computed()` for it.
- Sometimes you must *do something* when state changes: save, log or load data.
- **Side effect** = an action outside the screen, such as saving or calling a server.
- Vue gives you `watch()` and `watchEffect()` for side effects.

**Simple lesson:** `computed` makes values. Watchers do jobs.

### A Real-World Example

- Meera's bill shows the price, the plates, the GST and the total.
- GST and total must always match the plates.
- She does not want to update them by hand after every click.
- `computed()` keeps them right by itself.

const price = ref<number>(90);
const plates = ref<number>(2);

const subtotal = computed<number>(() => price.value * plates.value);
const gst = computed<number>(() => Math.round(subtotal.value * 0.05));
const total = computed<number>(() => subtotal.value + gst.value);
</script>

<template>
    <p>{{ plates }} × ₹{{ price }} = ₹{{ subtotal }}</p>
    <p>GST (5%): ₹{{ gst }}</p>
    <p><b>Total: ₹{{ total }}</b></p>
    <button @click="plates++">One more plate</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

- **What happened:** each click changed `plates`.
- `subtotal`, `gst` and `total` worked themselves out again.

### What You Will Learn

1. How to make a value with `computed()`.
2. Why computed values are cached, and how that differs from a function.
3. How to make a computed value you can also set.
4. How to run code when state changes with `watch()`.
5. The `deep`, `immediate` and `once` options.
6. How `watchEffect()` finds what to watch by itself.
7. When a watcher runs: `flush`.
8. How to stop a watcher and cancel old work.
9. When to pick computed and when to pick a watcher.

## computed()

- `computed(() => ...)` takes a function that returns a value.
- It gives back a read-only ref. Use `.value` in the script, the name in the template.
- Vue remembers which state the function read. These are its **dependencies**.
- When a dependency changes, the computed value is worked out again.

<figure className="vue-fig ngc-fig">
<svg viewBox="0 0 640 200" role="img" aria-labelledby="fig-vue-computed" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-vue-computed">Animation: price and plates feed into the computed subtotal. When plates changes, subtotal is worked out again and the screen updates. A watcher on total runs a side effect, writing to the console</title>
  <defs><marker id="vc-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0 0 L10 5 L0 10 z" style={{ fill: "var(--ngc-slate)" }} /></marker></defs>
  <g style={{ fontFamily: "var(--mono)", fontSize: 13, fontWeight: 700 }}>
    <rect x="20" y="30" width="120" height="44" rx="10" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 1.5 }} /><text x="80" y="57" textAnchor="middle" style={{ fill: "var(--ngc-blue)" }}>price</text>
    <rect x="20" y="110" width="120" height="44" rx="10" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 1.5 }} /><text x="80" y="137" textAnchor="middle" style={{ fill: "var(--ngc-blue)" }}>plates</text>
    <rect x="20" y="110" width="120" height="44" rx="10" opacity="0" style={{ fill: "none", stroke: "var(--ngc-orange)", strokeWidth: 3 }}><animate attributeName="opacity" values="0;1;1;0;0" keyTimes="0;0.05;0.25;0.3;1" dur="6s" repeatCount="indefinite" /></rect>
    <rect x="230" y="70" width="160" height="44" rx="10" style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)", strokeWidth: 1.5 }} /><text x="310" y="97" textAnchor="middle" style={{ fill: "var(--ngc-purple)" }}>computed subtotal</text>
    <rect x="230" y="70" width="160" height="44" rx="10" opacity="0" style={{ fill: "none", stroke: "var(--ngc-orange)", strokeWidth: 3 }}><animate attributeName="opacity" values="0;0;1;1;0;0" keyTimes="0;0.28;0.32;0.5;0.55;1" dur="6s" repeatCount="indefinite" /></rect>
    <rect x="480" y="30" width="140" height="44" rx="10" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)", strokeWidth: 1.5 }} /><text x="550" y="57" textAnchor="middle" style={{ fill: "var(--ngc-green)" }}>screen</text>
    <rect x="480" y="110" width="140" height="44" rx="10" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)", strokeWidth: 1.5 }} /><text x="550" y="137" textAnchor="middle" style={{ fill: "var(--ngc-orange)" }}>watch: save</text>
  </g>
  <path d="M140 52 L228 84" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2 }} markerEnd="url(#vc-arrow)" />
  <path d="M140 132 L228 100" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2 }} markerEnd="url(#vc-arrow)" />
  <path d="M390 84 L478 54" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2 }} markerEnd="url(#vc-arrow)" />
  <path d="M390 100 L478 130" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2, strokeDasharray: "5 4" }} markerEnd="url(#vc-arrow)" />
  <circle r="6" style={{ fill: "var(--ngc-orange)" }}><animateMotion dur="6s" repeatCount="indefinite" keyPoints="0;0;1;1" keyTimes="0;0.25;0.32;1" path="M140 132 L228 100" /></circle>
  <circle r="6" style={{ fill: "var(--ngc-green)" }} opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.55;0.56;0.7;0.71" dur="6s" repeatCount="indefinite" /><animateMotion dur="6s" repeatCount="indefinite" keyPoints="0;0;1;1" keyTimes="0;0.55;0.7;1" path="M390 84 L478 54" /></circle>
  <circle r="6" style={{ fill: "var(--ngc-orange)" }} opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.72;0.73;0.87;0.88" dur="6s" repeatCount="indefinite" /><animateMotion dur="6s" repeatCount="indefinite" keyPoints="0;0;1;1" keyTimes="0;0.72;0.87;1" path="M390 100 L478 130" /></circle>
  <text x="320" y="190" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--sans)", fontSize: 13 }}>plates changes → subtotal is worked out again → screen updates → watcher saves</text>
</svg>
<figcaption>Computed values feed the screen. Watchers run jobs on the side.</figcaption>
</figure>

### Computed vs a Function

- A function in the template runs again on **every** screen update.
- A computed value is **cached**. It runs again only when a dependency changes.
- **Cached** = the answer is saved and reused.
- Click "Refresh time" below. The function runs again. The computed value does not.

const dishes = ref<string[]>(["Dosa", "Idli", "Vada"]);
const now = ref<string>(new Date().toLocaleTimeString());

const countComputed = computed<number>(() => {
    console.log("computed ran");
    return dishes.value.length;
});

function countFunction(): number {
    console.log("function ran");
    return dishes.value.length;
}
</script>

<template>
    <p>Time: {{ now }}</p>
    <p>Computed: {{ countComputed }} dishes</p>
    <p>Function: {{ countFunction() }} dishes</p>
    <button @click="now = new Date().toLocaleTimeString()">Refresh time</button>
    <button @click="dishes.push('Pongal')">Add a dish</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

- **What happened:** "Refresh time" logged only "function ran".
- "Add a dish" changed a dependency, so both ran.

A computed function must only work out a value. Do not change other state, call a server or change the page inside it. Put those jobs in a watcher.

### Writable Computed

- A computed value is read-only by default.
- You can give it a `get` and a `set` to make it writable.
- Setting it runs `set`, which changes the real state.

const first = ref<string>("Meera");
const last = ref<string>("Nair");

const fullName = computed<string>({
    get: () => first.value + " " + last.value,
    set: (text: string) => {
      [first.value, last.value] = text.split(" ");
    },
});

function rename(): void {
    fullName.value = "Arjun Das";
}
</script>

<template>
    <p>Owner: {{ fullName }}</p>
    <p>First: {{ first }}, Last: {{ last }}</p>
    <button @click="rename">Change owner</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

**Simple lesson:** `computed()` works a value out from state and saves the answer until the state changes.

## watch()

- `watch(source, callback)` runs the callback when the source changes.
- The callback gets the **new** value and the **old** value.
- The source can be:
  - a ref
  - a **getter** (a function that returns a value)
  - a reactive object
  - an array of sources
- To watch one property of a reactive object, use a getter: `() => order.plates`.

const table = ref<number>(1);
const order = reactive({ plates: 1, dish: "Dosa" });

watch(table, (now, before) => {
    console.log(\`Table changed from \${before} to \${now}\`);
});

watch(
    () => order.plates,
    (now) => {
      console.log("Plates now:", now);
    },
);

watch([table, () => order.dish], ([t, d]) => {
    console.log(\`Table \${t} wants \${d}\`);
});
</script>

<template>
    <p>Table {{ table }}: {{ order.plates }} × {{ order.dish }}</p>
    <button @click="table++">Next table</button>
    <button @click="order.plates++">More plates</button>
    <button @click="order.dish = 'Idli'">Switch to Idli</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

- **What happened:** each button changed one source.
- Only the watchers for that source ran. Look at the **Console** below the preview.

`watch(order.plates, ...)` does not work. It passes the number 1, not something Vue can watch. Wrap it in a getter: `watch(() => order.plates, ...)`.

### deep, immediate and once

- **deep** = also notice changes inside nested objects and arrays.
- A watched reactive object is deep by default. A getter or ref to an object is not.
- `deep: 1` (a number, Vue 3.5) watches only one level down.
- Careful: when you change **inside** an object, the new and old values are the **same object**. Compare a copied value instead, such as a getter that returns `order.plates`.
- **immediate** = run the callback once right away, not only after a change.
- **once** = run the callback one time only, then stop (Vue 3.4).

const cart = ref<string[]>(["Dosa"]);
const table = ref<number>(4);

watch(cart, (list) => console.log("deep: cart is", list.join(", ")), { deep: true });

watch(table, (t) => console.log("immediate: serving table", t), { immediate: true });

watch(table, () => console.log("once: first table change seen"), { once: true });
</script>

<template>
    <p>Cart: {{ cart.join(", ") }}</p>
    <p>Table: {{ table }}</p>
    <button @click="cart.push('Vada')">Add vada</button>
    <button @click="table++">Next table</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

- **What happened:** "immediate" logged before any click.
- `push` changed the array inside the ref. `deep: true` noticed it.
- "once" logged on the first table change only.

**Simple lesson:** `watch()` runs your job when a chosen source changes.

## watchEffect()

- `watchEffect(fn)` runs `fn` right away.
- Vue tracks every reactive value the function reads.
- When any of them changes, the function runs again.
- You do not list the sources. Vue finds them.
- Trap: in an `async` function, Vue only tracks what is read **before the first `await`**. Read the values you need at the top.

const dish = ref<string>("Dosa");
const plates = ref<number>(1);

watchEffect(() => {
    console.log(\`Kitchen ticket: \${plates.value} × \${dish.value}\`);
});
</script>

<template>
    <p>{{ plates }} × {{ dish }}</p>
    <button @click="plates++">More</button>
    <button @click="dish = 'Upma'">Upma instead</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

### When a Watcher Runs: flush

- By default a watcher runs **before** Vue updates the screen. This is `flush: 'pre'`.
- `flush: 'post'` runs it **after** the screen update, so it can read the new page.
- `flush: 'sync'` runs it at once, on every single change. Use it rarely.
- `watchPostEffect()` and `watchSyncEffect()` are short names for these.

const status = ref<string>("Closed");

watch(status, () => {
    console.log("pre sees:", document.getElementById("s")?.textContent);
});

watchPostEffect(() => {
    console.log("post sees:", status.value, "/", document.getElementById("s")?.textContent);
});
</script>

<template>
    <p id="s">{{ status }}</p>
    <button @click="status = 'Open'">Open</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

**Simple lesson:** `watchEffect` watches whatever it reads. `flush: 'post'` waits for the new screen.

## Stopping and Cleaning Up

- A watcher stops by itself when its component is removed.
- But a watcher created later, inside a `setTimeout` or after an `await`, is not linked to the component. Stop it yourself.
- `watch()` and `watchEffect()` return a **stop** function. Call it to stop early.
- **Stale request** = an old request that finishes after a newer one and shows old data.
- `onWatcherCleanup(fn)` (Vue 3.5) runs `fn` before the watcher runs again, or when it stops.
- Use it to cancel the old request with `AbortController`.

interface Dish {
    id: number;
    name: string;
}

const id = ref<number>(1);
const dish = ref<string>("…");

const stop = watch(
    id,
    async (newId) => {
      const controller = new AbortController();
      onWatcherCleanup(() => {
        console.log("cancel request for dish", newId);
        controller.abort();
      });
      try {
        const res = await fetch(\`https://api.meeraskitchen.dev/dishes/\${newId}?delay=800\`, {
          signal: controller.signal,
        });
        dish.value = ((await res.json()) as Dish).name;
      } catch {
        // A cancelled request lands here. Nothing to show.
      }
    },
    { immediate: true },
);
</script>

<template>
    <p id="dish">Dish {{ id }}: {{ dish }}</p>
    <button @click="id = 2">Dish 2</button>
    <button @click="id = 3">Dish 3</button>
    <button @click="stop()">Stop watching</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

- **What happened:** click "Dish 2" then "Dish 3" quickly.
- The request for dish 2 was cancelled. Only dish 3 was shown.
- After "Stop watching", clicks change `id` but load nothing.

Real apps call a real server, which may take any amount of time. In this lab, the pretend API at `api.meeraskitchen.dev` answers after the `delay` you ask for, and logs each finished request in the Console.

**Simple lesson:** stop watchers you no longer need, and cancel old work in `onWatcherCleanup`.

## Computed or Watcher?

<div className="tscroll">

| Question | Use |
|---|---|
| Do I need a value to show? | `computed` |
| Do I need to save, log, call a server or touch the page? | `watch` or `watchEffect` |
| Do I need the old value too? | `watch` |
| Should it run at the start? | `watchEffect`, or `watch` with `immediate` |
| Is it many sources I do not want to list? | `watchEffect` |

</div>

**Simple lesson:** values are computed. Jobs are watched.

## TL;DR

- **`computed(() => x)`** = a cached, read-only value worked out from state.
- **Dependencies** = the state a computed or watcher reads. A change reruns it.
- **Writable computed** = `computed({ get, set })`.
- **`watch(source, cb)`** = runs `cb(new, old)` when the source changes.
- **Getter source** = `() => obj.prop`, to watch one property.
- **Options** = `deep`, `immediate`, `once`, `flush`.
- **`watchEffect(fn)`** = runs now, then again when anything it read changes.
- **Stop** = the function returned by `watch()` / `watchEffect()`.
- **`onWatcherCleanup(fn)`** = cancel old work before the next run (Vue 3.5).

## Common mistakes

<div className="tscroll">

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Side effects in computed | A computed can save or fetch | Use a watcher for jobs | Strange loops and repeated requests |
| `watch(obj.count, ...)` | Any value can be watched | Use a getter: `() => obj.count` | A warning, and the watcher never runs |
| Watcher for a simple total | Watch and set another ref | Use `computed` | More code, and values can drift apart |
| Ref to an array, no `deep` | `push` triggers `watch(list, ...)` | Add `{ deep: true }` | The watcher misses the change |
| Expecting a run at the start | `watch` runs once at the start | Add `{ immediate: true }` | Nothing happens until a change |
| Reading the page in a pre watcher | The DOM is already new | Use `flush: 'post'` | You read the old text |
| No cleanup for requests | Old replies do not matter | Abort in `onWatcherCleanup` | Old data replaces new data |
| Reading state after `await` in `watchEffect` | Everything it reads is tracked | Read sources before the first `await` | Changes are missed |
| Comparing new and old in a deep watch | Old keeps the previous object | Watch a getter of the value you compare | New and old look the same |

</div>

## Check Yourself

## Exercises

## What to Remember

- `computed` is for values. It is cached until a dependency changes.
- Never change state or call servers inside a computed.
- `watch` is for jobs. Use a getter to watch one property.
- `deep`, `immediate`, `once` and `flush` change when and how a watcher runs.
- Cancel old requests with `onWatcherCleanup` to avoid stale data.

Next: **Dynamic Rendering**
