Computed Signals
A computed signal is a read-only signal whose value is derived from one or more other signals. Instead of storing state, it describes how to calculate a value, and Angular keeps that result in sync automatically. Computed signals are lazy and memoized: they only recalculate when something they read actually changes, and only when their value is next read. This makes them the idiomatic way to model derived state without duplicating data or wiring up manual subscriptions.
Creating a computed signal
Call computed() with a derivation function. Inside that function you read any number of signals; whichever ones you read become the computed’s dependencies. The result is a Signal<T> you call like any other signal.
import { signal, computed } from '@angular/core';
const price = signal(100);
const quantity = signal(3);
const subtotal = computed(() => price() * quantity());
console.log(subtotal()); // reads dependencies and caches the result
quantity.set(5);
console.log(subtotal()); // recomputes because quantity changed
Output:
300
500
You never call .set() or .update() on a computed — its value is owned by the derivation function. The return type is a plain Signal<number>, not a WritableSignal, so the compiler prevents you from mutating it directly.
Laziness and memoization
Computed signals do not run eagerly. The derivation function executes the first time you read the signal, then caches the result. Subsequent reads return the cached value with no work at all. The function only re-executes when one of its dependencies changes and the computed is read again.
import { signal, computed } from '@angular/core';
const width = signal(4);
const height = signal(5);
const area = computed(() => {
console.log('computing area');
return width() * height();
});
console.log(area()); // logs "computing area", returns 20
console.log(area()); // cached, no log, returns 20
width.set(8);
console.log(area()); // logs "computing area", returns 40
Output:
computing area
20
20
computing area
40
This means a computed signal that is never read never runs, and one whose dependencies are stable never recomputes — a free performance win compared to recalculating in a getter on every change detection cycle.
Dynamic dependencies
Dependencies are tracked per execution, not declared up front. If a branch of your derivation does not run, the signals it would have read are not tracked. This keeps the dependency graph tight and accurate.
import { signal, computed } from '@angular/core';
const showTax = signal(false);
const subtotal = signal(200);
const taxRate = signal(0.2);
const total = computed(() =>
showTax() ? subtotal() * (1 + taxRate()) : subtotal()
);
While showTax() is false, total does not depend on taxRate — changing the tax rate will not invalidate the cached value. The moment showTax flips to true, the next read re-tracks taxRate and picks up its changes.
Computed signals in components
Computed signals shine in standalone components for presentation logic. Read them directly in the template with the new control flow; Angular updates only the bindings that depend on a changed value.
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-cart',
standalone: true,
template: `
<p>Items: {{ count() }}</p>
<p>Total: {{ total() | currency }}</p>
@if (isEmpty()) {
<p>Your cart is empty.</p>
}
<button (click)="add()">Add item</button>
`,
})
export class CartComponent {
protected readonly items = signal<number[]>([]);
protected readonly count = computed(() => this.items().length);
protected readonly total = computed(() =>
this.items().reduce((sum, price) => sum + price, 0)
);
protected readonly isEmpty = computed(() => this.count() === 0);
add(): void {
this.items.update(list => [...list, 25]);
}
}
Note that count, total, and isEmpty form a small graph: isEmpty depends on count, which depends on items. Updating items once correctly invalidates all three.
computed() vs. a getter or method
A common alternative is a class getter or template method call. Both recompute on every change detection pass, regardless of whether their inputs changed. A computed signal recomputes only when needed.
| Aspect | Method / getter | computed() |
|---|---|---|
| Recomputation | Every CD cycle | Only when a dependency changes |
| Caching | None | Memoized result |
| Dependency tracking | Manual / none | Automatic on read |
| Zoneless friendly | No | Yes |
| Write access | N/A | Read-only by design |
Custom equality
Like writable signals, a computed uses Object.is by default to decide whether its new result differs from the cached one. If the derivation produces value objects, supply an equal function so downstream consumers are not notified for equivalent results.
import { signal, computed } from '@angular/core';
const user = signal({ first: 'Ada', last: 'Lovelace' });
const fullName = computed(
() => `${user().first} ${user().last}`,
{ equal: (a, b) => a === b }
);
Warning: Keep derivation functions pure. Do not call
.set()/.update()on other signals, perform HTTP requests, or trigger side effects insidecomputed()— Angular throws if you write to a signal during computation. Use effects for side effects instead.
Best Practices
- Use
computed()for any value that can be derived from existing signals rather than storing it as separate writable state. - Keep derivation functions pure and synchronous — no side effects, no signal writes, no async work.
- Let computed signals compose: build small, focused derivations that read other computeds instead of one large function.
- Prefer a
computedover a template method or getter so work runs only when dependencies actually change. - Supply a custom
equalfunction when the result is an object whose identity changes but whose meaning does not. - Read computed signals directly in templates; they integrate with zoneless change detection for fine-grained updates.