The problem: derived data that drifts
In Module 1 you stored state and changed it by hand. But most values in a real app aren't stored — they're derived. A cart subtotal is quantity × price. A "form is valid" flag is a function of the fields. The naive approach is to recalculate derived values yourself whenever a source changes… and that's a bug factory.
// ❌ Imperative sync — derived data drifts out of date.
class Cart {
quantity = signal(2);
unitPrice = 12.5;
subtotal = 25; // a plain field we try to keep in sync by hand
setQuantity(q: number) {
this.quantity.set(q);
// Forget this one line and subtotal is silently wrong forever.
this.subtotal = this.unitPrice * q;
}
}Every place that mutates a source now has to remember to re-sync every derived value. Miss one and your UI shows stale data with no error. This is one of the most common bugs in front-end code.
Creating a computed signal
Call computed() with a function that reads other signals and returns a value. You read the result just like any signal — by calling it.
import { signal, computed } from '@angular/core';
const quantity = signal(2);
const unitPrice = signal(12.5);
// Declare WHAT the value is, never WHEN to recalculate it.
const subtotal = computed(() => unitPrice() * quantity());
subtotal(); // 25
quantity.set(3);
subtotal(); // 37.5 — recalculated for you, automaticallyA chain of derivations
Computeds can build on other computeds, forming a dependency graph. Here subtotal feeds tax, and both feed total. You only ever change quantity or the tax toggle — the three derived values keep themselves correct.
readonly quantity = signal(2);
readonly taxRatePct = signal(20);
private readonly unitPrice = signal(12.5);
// A chain of derivations — each reads the ones above it.
readonly subtotal = computed(() => this.unitPrice() * this.quantity());
readonly tax = computed(() => this.subtotal() * this.taxRatePct() / 100);
readonly total = computed(() => this.subtotal() + this.tax());
// Change quantity once and all three stay correct — forever.Live derived cart
Change quantity or tax. Every derived value below updates automatically — you write zero sync code.
Notice: subtotal, tax and total are never assigned to — they're pure formulas.
Lazy & memoized: it does the least work possible
A computed has two superpowers that make it cheap to use liberally:
- •Lazy — the formula doesn't run until something actually reads the value. An unread computed does not execute its formula, though creating it still allocates state.
- •Memoized — after it runs, the result is cached. Read it ten times and the body runs once. It only re-runs when a dependency has changed and the value is read again.
let runs = 0;
const source = signal(1);
const doubled = computed(() => { runs++; return source() * 2; });
doubled(); doubled(); doubled(); // runs === 1 (memoized — cached)
source.set(2); // runs === 1 (LAZY — nothing yet)
doubled(); // runs === 2 (recomputes on read)Count the recomputations
A counter inside the formula tracks how often its body actually executes.
Interact with the buttons to see when the body runs.
Only "Change source" ever increases the run count. Reading the cached value and touching unrelated state cost nothing.
Dependencies are tracked automatically — and dynamically
You never declare a computed's dependencies. Angular records exactly which signals you read while the formula runs, and re-tracks them every single run. That means dependencies can change over time based on your logic:
const useA = signal(true);
const a = signal(1);
const b = signal(2);
const result = computed(() => useA() ? a() : b());
result(); // establish the initial dependencies
// While useA() is true, result depends on useA and a — NOT b.
b.set(99); // result does not recompute; b wasn't read
useA.set(false); // invalidates result; dependencies change on its next evaluation
result(); // now tracks useA and b instead
// Dependencies are re-tracked on every run: only what you READ counts.You can't write to a computed
This is a feature, not a limitation. A computed's value is fully determined by its inputs, so writing to it directly would be meaningless. If you find yourself wanting to, that's a signal (pun intended) that you need either a plain writable signal or a linkedSignal().
const total = computed(() => subtotal() + tax());
total(); // ✅ read it
total.set(999); // ❌ compile error — computed signals are read-only
// A computed's value is owned by its formula. Need a derived value
// you can also override? That's linkedSignal() — coming in Module 7.🎯 Your turn: unlock free shipping
Items cost £12.5 each and free shipping kicks in at £50. There's no "syncing" here — a cartValue computed derives from quantity, and a qualifiesForFreeShipping computed derives from that. Add items until you qualify. The badge is driven entirely by the second computed.
Free shipping
Goal: cartValue() reaches the £50 threshold. Watch the derived-of-derived value flip.
Key takeaways
- ✓
computed(fn)declares derived state — describe the value, never re-sync it. - ✓A computed is a read-only signal; it has no
set()/update(). - ✓It's lazy (runs on read) and memoized (cached until a dependency changes).
- ✓Dependencies are tracked automatically from what you read — and re-tracked every run.
- ✓Keep the formula pure: no side effects. Those come next.