In an Angular component, why derive a cart total with computed() instead of a getter or a method called from the template?
answer
- runs only when first read
- cached until an input changes
- dependencies found by reading
- read-only, and no writes inside
basics
~20 scomputed() is lazy and memoized: its function runs on first read and its result is cached until a signal it read changes. A template getter or method reruns on every check of that view, and nothing caches its result.
solid answer
~40 sA `computed()` wraps a pure derivation such as `items().reduce(...)`. It is **lazy**, so the function does not run until something reads the signal, and **memoized**, so later reads return the cached result until one of the signals it read during its last run changes. Dependencies are tracked automatically and dynamically: only signals actually read in the last run count. A getter or template method that reads signals is still reactive, but it recalculates every time Angular checks that view, and every other caller recalculates it again. `computed()` also returns a read-only `Signal`, so nobody can `set()` a total that disagrees with its items, and writing to another signal inside it throws `NG0600`. That makes it the natural home for totals, counts and filtered lists.
code
ts · 29 linesimport {Component, computed, signal} from '@angular/core';
interface LineItem {
sku: string;
unitPrice: number;
qty: number;
}
@Component({
selector: 'app-cart-summary',
template: `
<p>{{ itemCount() }} items</p>
<p>Subtotal: {{ subtotal() }}</p>
@if (freeShipping()) {
<p>Free shipping</p>
}
`,
})
export class CartSummary {
readonly items = signal<LineItem[]>([]);
readonly subtotal = computed(() =>
this.items().reduce((sum, i) => sum + i.unitPrice * i.qty, 0),
);
readonly itemCount = computed(() =>
this.items().reduce((n, i) => n + i.qty, 0),
);
readonly freeShipping = computed(() => this.subtotal() >= 50);
}go deeper
Recall that computed() derives a read-only value from other signals, runs on first read, and caches its result until a signal it read changes.
Explain dynamic dependency tracking, why a getter is reactive but uncached, and why writes inside a computed() throw NG0600.
Show where computed chains pay off in a real screen, how cached errors and untracked plain fields cause confusing bugs, and how you keep derivations pure in review.
Weigh a team rule of deriving everything through computed() against readability and debugging costs, and where plain functions of signal values are still clearer.
## The problem a cart total poses A cart holds **line items**, each with a unit price and a quantity, and the page shows a subtotal, an item count and perhaps a free-shipping flag. None of those should be stored separately: they are **derived state**, a function of the items. The question is where that function lives in an Angular 22.2 component. Three candidates are common: 1. a **method** called from the template, `{{ subtotal() }}` where `subtotal()` is a class method; 2. a **getter**, `get subtotal() { ... }`, read as `{{ subtotal }}`; 3. a **`computed()` signal**, `subtotal = computed(() => ...)`, read as `{{ subtotal() }}`. ## What `computed()` does `computed()` from `@angular/core` takes a derivation function and returns a read-only `Signal<T>`: ```ts readonly items = signal<LineItem[]>([]); readonly subtotal = computed(() => this.items().reduce((sum, i) => sum + i.unitPrice * i.qty, 0), ); ``` Its behaviour has four parts: - **Lazy.** The function does not run when `computed()` is called. It runs the first time something reads `subtotal()`. - **Memoized.** The result is cached. Further reads return the cached value without re-running the function until a dependency changes. - **Automatic, dynamic dependencies.** Every signal the function reads while it runs becomes a dependency. There is no dependency list to maintain, and only the signals read *in the last run* count: a signal read only inside a branch that was not taken is not a dependency until that branch runs. - **Read-only.** A computed signal has no `set()` or `update()`; calling `subtotal.set(0)` is a compile error. When `items` changes, the cached `subtotal` is no longer valid, and the next read recomputes it. Reads of `subtotal()` in the template register the view as a consumer, so the view is refreshed when the total really changes. ## What a getter or method does instead A method or getter that reads `items()` from a template is still reactive: the read happens while the template is evaluated, so the view depends on `items`. What it lacks is the cache. | Aspect | Template method / getter | `computed()` | |---|---|---| | When it runs | every time Angular checks the view, plus every other call site | on first read, then only after a dependency changed | | Result cached | no | yes, until a dependency changes | | Shared between readers | each caller recomputes | one cached value for every reader | | Can be overwritten | a setter could be added | no `set()`; read-only by type | | Writes inside allowed | writing during template evaluation throws `NG0600` | writing inside throws `NG0600` | For a three-item cart the cost difference is invisible. It matters when the derivation filters or sorts a long list, when the same total is read in several places, or when a view is checked often. With `computed()`, the guide's advice holds: expensive derivations such as filtering arrays are safe to put there. ## Rules that keep `computed()` honest - **Keep the function pure.** It should read signals and return a value. Setting another signal inside it throws `NG0600` ("Writing to signals is not allowed in a `computed`"). If the value must also be writable locally, that is `linkedSignal()`'s job; if a change must trigger a side effect, that is an effect's. - **Read every input through a signal.** A plain class field read inside a `computed()` is not tracked; changing it does not invalidate the cache. - **Do not reference the computed from itself.** A cycle is detected and throws "Detected cycle in computations." in dev mode. - **Errors are cached too.** If the function throws, the error is stored and rethrown on every read until a dependency changes and the function runs again. - **Return new objects only when something changed**, or give the computed an `equal` option, if downstream consumers should not see a fresh reference each time. ## A worked cart example ```ts readonly itemCount = computed(() => this.items().reduce((n, i) => n + i.qty, 0), ); readonly freeShipping = computed(() => this.subtotal() >= 50); ``` `freeShipping` depends on `subtotal`, which depends on `items`. Changing one quantity marks the chain stale; reading `freeShipping()` pulls a fresh `subtotal` first. If `subtotal` comes out unchanged, `freeShipping` keeps its cached value and does not rerun. ## What interviewers listen for - "Lazy **and** memoized", not just "cached". - Dependencies discovered **by reading**, not declared. - A getter is reactive but uncached; it is not "non-reactive". - No writes inside a `computed()`, and why that keeps derived state consistent.
- In Angular, what happens if the function passed to computed() throws?The error is caught and cached as the computed's state, and every read rethrows it. It keeps rethrowing until one of the signals read during that failed run changes, which marks the computed stale so the next read runs the function again.
- In Angular, why does a class field read inside computed() cause stale totals?Only signal reads are tracked. A plain field, say `this.taxRate`, is read but not recorded as a dependency, so changing it never invalidates the cached value. Put the input in a signal, or derive it from one, so the computed sees it change.
- In Angular, what happens when code inside computed() calls set() on another signal?Angular throws `NG0600`, whose message says writing to signals is not allowed in a `computed`. Derivations must be pure. Model the second value as its own `computed()`, or move the write to the event handler or effect where the change really originates.
saying these in an interview costs you the question
- A getter that reads signals in a template is not reactive at all.
- computed() runs its function immediately when it is created.
- You must list a computed's dependencies, like a dependency array.
- A computed can be overridden with set() when the user edits the total.
- computed() re-runs on every change detection pass, just like a method.