skip to content

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%

answer

  1. the getter is not argument-free anymore
  2. undefined on the very first run
  3. Object.is on the new result
  4. a fresh object every time

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.

solid answer

~40 s

Since Vue 3.4 a computed getter is called as `getter(previous)`, where `previous` is the value it returned last time and `undefined` on the first evaluation; for a writable computed, `get(previous)` receives it the same way. That lets a getter keep its last acceptable value, for example returning `previous` while an input is out of range. Also since 3.4, a computed that recomputes only notifies the effects reading it when the new result differs from the old one by `Object.is`: `isEven` going `true` to `true` re-runs nothing. That identity check does not see inside objects, so a getter that builds a new object every time always counts as changed. One trap: dependencies are re-collected on every run, so a getter that returns `previous` *before* reading its sources stops tracking them and never updates again.

code

ts · 14 lines
ts
import { ref, computed } from 'vue'

const input = ref('12')

// Keep showing the last valid number while the user is mid-typing
const parsed = computed<number | undefined>((previous) => {
  const n = Number(input.value) // read the source first, every run
  return Number.isNaN(n) ? previous : n
})

input.value = '12a'
console.log(parsed.value) // 12, kept from the previous run
input.value = '15'
console.log(parsed.value) // 15

go deeper

for a junior

Know that since Vue 3.4 a computed getter can take its previous value as an argument, which is undefined on the first run.

for a middle

Explain the Object.is comparison that decides whether dependents re-run, and why a getter that returns a new object every time never benefits from it.

for a senior

Catch getters that short-circuit to the previous value before reading their sources, and explain how per-run dependency collection makes them freeze.

for a principal

Weigh relying on version-specific computed semantics in shared code, and document the 3.4 floor where a library or team depends on them.

## Two 3.4 changes that belong together Vue 3.4 changed two things about `computed()` that are easiest to understand as a pair: - **The previous-value argument.** The getter receives the value it returned last time. - **Computed stability.** A recomputed value only notifies its dependents when it actually changed. Both are about the relationship between one run of the getter and the next. ## The previous-value argument A getter can declare a parameter. Vue passes the computed's last returned value, or `undefined` on the first evaluation: ```ts import { ref, computed } from 'vue' const count = ref(2) // Returns count while it is <= 3; otherwise keeps the last value that was const alwaysSmall = computed((previous) => { if (count.value <= 3) { return count.value } return previous }) ``` With a writable computed the same argument arrives in `get(previous)`, while `set(value)` still receives the assigned value. Typical uses: 1. Keep the last valid result while input is temporarily invalid (a half-typed number, an out-of-range slider). 2. Return the previous object when nothing meaningful changed, so its identity stays the same. 3. Accumulate across runs in a controlled way, for example the highest value seen so far. ## Computed stability Before 3.4, when a dependency changed, anything reading the computed was triggered even if the getter then produced the same result. Since 3.4, Vue recomputes first and compares: only if the new value differs from the old one (the check is `Object.is`, via Vue's internal `hasChanged`) does it notify the effects and components that read it. ```ts import { ref, computed, watchEffect } from 'vue' const count = ref(0) const isEven = computed(() => count.value % 2 === 0) watchEffect(() => console.log(isEven.value)) // logs true count.value = 2 // isEven is still true: no log count.value = 4 // still true: no log ``` | Getter returns | Result after a dependency change | Dependents re-run? | |---|---|---| | same primitive (`true` then `true`) | equal by `Object.is` | no | | different primitive | not equal | yes | | a new object or array each run | never equal by identity | yes, every time | | `previous` when nothing changed | same identity | no | ## Where it does not help `Object.is` compares identity, not contents. A getter such as `() => ({ isEven: count.value % 2 === 0 })` builds a new object on every run, so it always looks changed. Vue deliberately does not deep-compare, because a deep comparison on every recompute could cost more than it saves. Returning `previous` when its contents match is the manual way to keep identity stable; tuning that for render performance is a performance topic rather than a question of what `computed()` does. ## The trap: returning previous too early Dependencies are **re-collected on every run**. After each evaluation Vue drops the dependencies the getter did not read this time. So the order inside the getter matters: - Safe: read every source first, then decide whether to return `previous`. - Broken: `computed((prev) => prev ?? expensiveSum(items.value))`. The first run reads `items`. When `items` changes, the second run finds `prev` defined and returns it without reading `items`; that run records no dependencies, and the computed never updates again. The Vue documentation makes the same point: perform the full computation before comparing and returning the old value, so the same dependencies are collected on every run. ## A short checklist for getters that use previous - Read every reactive source at the top of the getter, before any early return. - Treat `previous` as possibly `undefined`; the first run always receives nothing. - Decide whether to return `previous` from values you have just read, never as a shortcut that skips reading the sources. - Keep the getter pure: `previous` is a value to compare or reuse, not a place to accumulate mutations. - Remember the argument only exists from 3.4 on; code that must run on older 3.x versions cannot rely on it. ## Why interviewers ask it These features are recent and easy to miss, so they separate candidates who follow Vue's changes from those who learned computed once. The follow-up usually probes whether you understand that the comparison is by identity, and whether you can see the dependency trap in a getter that short-circuits.

  • A Vue computed returns { isEven } as a new object on every run. Does 3.4 stability stop its watchers from re-running?
    No. The comparison is `Object.is`, and a new object is never identical to the old one, so every recompute counts as a change. Vue does not deep-compare. You can compare the contents yourself inside the getter and return `previous` when they match, keeping the same identity.
  • Why does computed(prev => prev ?? sum(items.value)) stop reacting to items?
    Dependencies are re-collected on each run. When `items` changes, the rerun returns `prev` without reading `items`, so that run records no dependencies and nothing can invalidate the computed afterwards. Read the sources first and only then decide whether to return the previous value.

saying these in an interview costs you the question

  • A computed getter never receives any argument in Vue 3.
  • Since 3.4 Vue deep-compares computed objects before notifying.
  • A computed always triggers its dependents whenever any dependency changes.
  • Returning previous early is harmless because dependencies are remembered forever.
  • The previous value is the component instance, as with this in options.