skip to content

Computed & Watchers

Vue derives state with cached computed() and reacts with watch and watchEffect, whose flush timing and cleanup differ. Interviewers ask when a watcher that sets state should have been a computed.

part ofVue.jsoverview, primer and where to startread it →
on this pageshow

explore

questions

17

In Vue 3, why use computed() for a cart total instead of a method called from the template?

level: juniorimportance: must knowfreq 80%

answer

  1. one runs per render, one does not
  2. cached until something it read changes
  3. only reactive reads count
  4. Date.now() inside a getter

basics

~20 s

computed() caches its result and re-runs its getter only after a reactive value it read has changed; a method called in the template has no cache and runs again on every re-render of the component.

solid answer

~40 s

`computed()` returns a readonly ref whose getter Vue tracks: every reactive value the getter reads becomes a dependency, and the cached result is reused until one of those dependencies changes. A method such as `cartTotal()` called from the template has no cache, so it runs on every render of the component, including renders caused by unrelated state such as an open dropdown. For a total that loops over every line item, and for further computeds built on that total, the cache saves repeated work. The caching cuts both ways: only *reactive* reads count, so `computed(() => Date.now())` never updates. Use `computed` for a pure derivation of reactive state; use a method when you need an argument or deliberately want a fresh call each time.

code

vue · 20 lines
vue
<script setup>
import { ref, computed } from 'vue'

const items = ref([{ price: 49, qty: 1 }, { price: 9, qty: 3 }])
const open = ref(false)

const total = computed(() =>
  items.value.reduce((sum, i) => sum + i.price * i.qty, 0)
)

function totalByMethod() {
  return items.value.reduce((sum, i) => sum + i.price * i.qty, 0)
}
</script>

<template>
  <button @click="open = !open">Toggle</button>
  <p>{{ total }}</p>
  <p>{{ totalByMethod() }}</p>
</template>

go deeper

for a junior

Say that a computed is cached by its reactive dependencies and a template method call runs on every render, and give one example where that difference matters.

for a middle

Explain how the dependency set is recorded from reactive reads during the getter, why non-reactive reads such as Date.now() never invalidate it, and why the getter must be pure.

for a senior

Show where caching pays off in a real component, such as chains of derived totals, and when a method or a keyed map is the honest choice for argument-dependent values.

for a principal

Frame the choice as a team convention: derived state lives in computeds, side effects in watchers, and review flags template method calls that loop over large collections.

## What computed() gives you In Vue 3, `computed()` (imported from `vue`) takes a **getter function** and returns a **computed ref**: a readonly ref whose `.value` is whatever the getter last returned. In a `<script setup>` component you read it as `total.value` in script and as `total` in the template, where refs are unwrapped automatically. The important part is not the ref, it is the **cache**. While the getter runs, Vue records every reactive value it reads (a `ref`'s `.value`, a property of a `reactive()` object, another computed). Those reads become the computed's **dependencies**. Until one of them changes, reading `.value` returns the stored result without calling the getter again. ## Computed versus a method in the template A method is just a function. If the template contains `{{ cartTotal() }}`, the call is part of the component's render, and Vue re-runs the render whenever any reactive state the render reads changes. Every such render calls the method again, even when the cart items did not change. | | `computed()` | method called in the template | |---|---|---| | When the logic runs | after a dependency changed, on the next read | on every render of the component | | Result reused between renders | yes, cached | no | | Takes arguments | no | yes | | Other code can build on it | yes, other computeds and watchers can read it | only by calling it again | | Good for | pure derivations of reactive state | per-call work, values that need arguments | For a cart, the difference is concrete: - The component also holds unrelated state (a promo-code input, an open dropdown, a hover flag). - Each keystroke in the promo input re-renders the component. - With a method, every keystroke re-sums every line item. - With `computed`, the sum runs once per change to the items and is reused otherwise. The saving compounds when other derived values build on the first one: `tax`, `shipping` and `grandTotal` computeds all read `subtotal`, and none of them re-runs while the items are unchanged. ## Where the cache surprises people The cache is keyed by **reactive** reads only. Three consequences follow: 1. `computed(() => Date.now())` never updates, because `Date.now()` is not reactive; nothing can invalidate the cached value. Store the time in a ref that a timer updates instead. 2. A plain `let` variable or a non-reactive module constant read by the getter is not a dependency. Changing it does not refresh the computed. 3. Dependencies are collected on each run, so a branch that was not taken (`if (!showTax.value) return base`) does not track the reads inside it until a later run takes that branch. ## The getter must stay pure Because Vue decides *when* a getter runs, the getter must not do anything whose timing matters. The Vue documentation is explicit: a computed getter should only compute and return a value. It should not mutate other state, start an async request, or touch the DOM. Those are **side effects**, and they belong in a watcher or an event handler. Likewise, treat the value a computed returns as read-only: to change a derived list, change the source state it is derived from. ## When a method is still the right tool - The logic needs an argument, such as the subtotal of one category: a computed getter takes no caller arguments. - You deliberately want a fresh result on every call, for example a random pick or the current time at the moment of rendering. - The work is trivial and reads nothing worth caching. ## A worked example ```vue <script setup> import { ref, computed } from 'vue' const items = ref([ { name: 'Keyboard', price: 49, qty: 1 }, { name: 'Cable', price: 9, qty: 3 } ]) const promo = ref('') // Re-runs only when items (or an item's price/qty) change const subtotal = computed(() => items.value.reduce((sum, i) => sum + i.price * i.qty, 0) ) // A method: runs on every render, which is fine for cheap formatting function money(n) { return n.toFixed(2) } </script> <template> <input v-model="promo" /> <p>Subtotal: {{ money(subtotal) }}</p> </template> ``` Typing in the promo field re-renders the component and calls `money()` again, but `subtotal` returns its cached number without looping over the items.

  • Can a Vue computed take an argument, such as the subtotal for one category?
    No. The getter receives no caller arguments (since 3.4 it receives only its own previous value). Returning a function from a computed gives you something callable, but only the function is cached, not its results, so each call recomputes. Either compute a map of subtotals keyed by category in one computed, or use a method when an argument is genuinely needed.
  • Why does computed(() => Date.now()) show the same time forever?
    The cache is invalidated only by reactive dependencies, and `Date.now()` is not reactive, so after the first evaluation nothing can mark the value stale. Keep the current time in a `ref` that a timer updates, and derive from that ref instead.
  • What must never happen inside a Vue computed getter?
    Side effects: mutating other state, starting an async request, or changing the DOM. Vue decides when the getter runs, so any effect inside it runs at unpredictable times or not at all. Put side effects in a watcher or an event handler and keep the getter a pure derivation.

saying these in an interview costs you the question

  • A computed and a method run equally often; the difference is only syntax.
  • A computed re-evaluates on every render of the component.
  • computed(() => Date.now()) keeps updating as time passes.
  • Fetching data or setting another ref inside a computed getter is fine.
  • A computed needs a dependency list to know when to recompute.
open as a page

In Vue 3, when does watchEffect() run its function, and which reactive reads become its dependencies?

level: juniorimportance: must knowfreq 62%

basics

~20 s

With the default flush, watchEffect() runs its function immediately, and its dependencies are every reactive value read during that synchronous run. It re-runs when any of them changes, re-collecting dependencies each time, so conditional reads come and go.

open as a page

In a Vue 3 `<script setup>` component, why does `watch(props.userId, cb)` never fire, and how should you watch the prop?

level: juniorimportance: must knowfreq 72%

basics

~20 s

props.userId is read once and passes a plain value, not a reactive source, so Vue warns and tracks nothing. Pass a getter, () => props.userId, so the read happens inside the watcher where Vue can track it.

open as a page

In Vue 3, which source forms does watch() accept, and how does each form decide whether the callback runs?

level: middleimportance: must knowfreq 68%

basics

~20 s

Vue 3's watch() accepts a ref, a reactive object, a getter function, or an array of these. A ref or getter fires when its value changes by Object.is; a reactive object is watched deeply; an array fires when any entry changes.

open as a page

In Vue 3.5, what do watch() and watchEffect() return, and when would you stop, pause or resume a watcher yourself?

level: juniorimportance: should knowfreq 45%

basics

~20 s

They return a WatchHandle: call it, or its stop(), to end the watcher for good, and since Vue 3.5 use pause() and resume() to suspend it. Watchers created synchronously in setup stop automatically on unmount, so manual stops are for other cases.

open as a page

How do you make a Vue 3 computed() writable, and what happens when you assign to a getter-only one?

level: middleimportance: should knowfreq 45%

basics

~10 s

Pass computed() an object with get and set; assigning to .value calls set, which should update the source refs. Assigning to a getter-only computed changes nothing and, in development builds, logs a readonly warning.

open as a page

In Vue 3, how do watch()'s deep, immediate and once options change when and how often the callback runs?

level: middleimportance: should knowfreq 60%

basics

~20 s

deep makes a ref or getter source fire on nested mutations, and since 3.5 a number caps traversal depth; immediate runs the callback at creation with an undefined old value; once (3.4+) stops the watcher after its first callback.

open as a page

In Vue 3, why does an async watchEffect() ignore changes to a ref it reads after an await, and how do you fix it?

level: middleimportance: should knowfreq 45%

basics

~20 s

watchEffect() tracks only reads made during its synchronous run; an async function's run ends, from Vue's view, at the first await. Reads after it happen with no active effect. Read every dependency before the first await, or use watch() with explicit sources.

open as a page

In Vue 3, what do the watcher flush options 'pre', 'post' and 'sync' mean, and which one is the default?

level: middleimportance: should knowfreq 50%

basics

~20 s

'pre' (the default) runs a watcher after parent updates but before its owner component's DOM update; 'post' runs it after Vue has updated the DOM; 'sync' runs it immediately on every change, with no batching. watchPostEffect and watchSyncEffect are the aliases.

open as a page

In Vue 3, why can a watch callback receive a newValue identical to oldValue, and how do you get a genuine previous value?

level: middleimportance: should knowfreq 52%

basics

~20 s

Deep and reactive-object watchers fire on in-place mutation while the source keeps returning the same object, so Vue passes that one object as both values. For a real previous value, watch a primitive or return a fresh copy from a getter.

open as a page

A Vue 3 component creates a watch() after an await inside onMounted, and it keeps firing after the component unmounts; why, and how do you fix it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Watchers are bound to the component only when created while its scope is active, which ends at the first await. The late watcher belongs to no scope, so unmount never stops it. Create it synchronously with a condition, or keep its handle and stop it.

open as a page

A Vue 3 cart keeps total in a ref that a watch() on items updates; what goes wrong, and what should replace it?

level: seniorimportance: should knowfreq 55%

basics

~20 s

The watched copy is extra state that drifts: wrong until the callback first runs, stale when a source is unwatched, and briefly stale after each mutation. A computed derived from the items has none of these gaps.

open as a page

A Vue 3 component redraws a canvas from reactive points in a default watchEffect(); the canvas is null on the first run and drawings vanish on resize - why, and what do you change?

level: seniorimportance: should knowfreq 35%

basics

~20 s

A default watchEffect() first runs synchronously in setup, before the canvas exists, and later runs before the DOM update, so a width change patched afterwards clears the drawing. Use watchPostEffect() so every draw runs after Vue has updated the DOM.

open as a page

A Vue 3 page runs watch(filters, loadResults) on a reactive filters object; it refetches when an unrelated field changes and loads nothing initially. How do you fix the watcher?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Passing the reactive object makes the watch implicitly deep, so any nested edit refetches, and watch is lazy. Watch an array of getters for only the query's fields, with immediate: true for the first load.

open as a page

In Vue 3, how does onWatcherCleanup() differ from the onCleanup argument a watcher receives, and why can onWatcherCleanup fail inside an async callback?

level: middleimportance: nice to knowfreq 32%

basics

~20 s

onWatcherCleanup(), new in Vue 3.5, is imported and attaches to whichever watcher is currently running, so it only works synchronously. The onCleanup argument is bound to its own watcher, so it still registers after an await.

open as a page

Since Vue 3.4, what does a computed() getter receive as its argument, and when does the computed notify its dependents?

level: middleimportance: nice to knowfreq 28%

basics

~20 s

Since Vue 3.4, the getter receives the computed's previous value (undefined on the first run), and a recomputed result notifies dependents only if it differs from the previous one by Object.is, so an unchanged result triggers nothing.

open as a page