Angular 48 🅰️ Computed Signals
A computed signal is a derived value. It reads other signals — called its dependencies — and produces a value from them. When any dependency changes, the computed signal marks itself as stale, and the next read recomputes the value. This is the mechanism that turns a set of independent signals into a graph, where the values flow from the sources through the derived nodes to the consumers. The computed function is one line in its simplest form, and it is the most-used signal API after signal itself. But the model behind it — the lazy evaluation, the memoization, the dependency tracking, the glitch-free consistency — is what makes it reliable and efficient. This chapter covers the computed function in detail: how to write it, what it depends on, when it recomputes, the caching behavior, the difference between a computed and a method, the composition of computed values, and the patterns that make signal-based derivations predictable.
Key point: A computed signal is created with computed(fn), where fn is a function that reads signals and returns a value. The computed tracks the signals the function reads — the reads are recorded during the computation — and it recomputes when any of them changes. The recomputation is lazy: the function runs only when the computed is read, not when a dependency changes. The result is cached: if no dependency has changed since the last read, the cached value is returned. The function must be pure — no side effects, no writes to signals it reads. Computeds can depend on other computeds, and the graph is transitively tracked. A computed is read-only: it has no set or update, and the only way to change its value is to change its dependencies.
What a computed signal is
A computed signal is a read-only signal whose value is derived from other signals. It is created with the computed function, and its value is read with () like any other signal.
import { signal, computed } from '@angular/core';
const price = signal(100);
const quantity = signal(2);
const subtotal = computed(() => price() * quantity());
console.log(subtotal()); // 200
price.set(150);
console.log(subtotal()); // 300
The subtotal reads price and quantity. The reads are recorded, and when either changes, the subtotal is marked as stale. The next read recomputes the value.
Why the computed is read-only. The value is derived, and the only way to change it is to change its dependencies. The read-only nature is what makes the graph a directed acyclic graph — the values flow in one direction, from the sources to the derived values. A computed that could be set would break the model.
Why the reads are the dependencies. The computed’s function reads the signals, and the reads are recorded. The dependency list is the set of signals read during the computation. A conditional read — a signal read only in one branch — is a dependency only when the branch runs. The dynamic dependency list is a feature of the model.
Why the computed is a signal. A computed has the same read API as a signal. It can be read in the template, in another computed, in an effect. It is a signal for all the purposes the consumer cares about, and the read-only nature is the only difference.
Why the computed is useful. A derived value that is computed by hand — a method that reads the signals — recomputes on every call, even when the inputs have not changed. The computed caches the result and recomputes only when the inputs change. The efficiency is the main benefit.
Why the computed is the modern replacement for a getter. A class getter get total() { return this.price * this.quantity; } computes on every read. A computed signal caches the result. In a template that reads the value frequently, the difference is significant.
Why the computed is the replacement for many RxJS combinations. A combineLatest of two BehaviorSubjects, piped through a map, produces a derived stream. The same derivation with signals is a computed. The signal version is synchronous, tracks its dependencies automatically, and does not need a subscription. The computed is the modern answer for the derived state.
The dependency tracking
The dependency tracking is the mechanism that makes the computed work. The function reads the signals, and the reads are recorded.
const a = signal(1);
const b = signal(2);
const sum = computed(() => {
console.log('computing');
return a() + b();
});
console.log(sum()); // computing, 3
console.log(sum()); // 3 (cached, no computing)
a.set(10);
console.log(sum()); // computing, 12
b.set(20);
console.log(sum()); // computing, 30
The first read runs the computation. The second read returns the cache. The change to a marks the computed as stale, and the next read recomputes. The change to b does the same.
Why the reads are tracked automatically. The signal’s read records the current computation context, and the dependency is added. The developer does not declare the dependencies; the reads are the declaration. The model is automatic, and it cannot get out of sync with the code.
Why the conditional read changes the dependency list. A signal that is read only in one branch is a dependency only when that branch runs.
const useA = signal(true);
const a = signal(1);
const b = signal(2);
const value = computed(() => (useA() ? a() : b()));
console.log(value()); // 1 (a is read, b is not)
useA.set(false);
console.log(value()); // 2 (b is read, a is not)
After useA.set(false), the computed depends on useA and b, not on a. A change to a does not recompute the value, and a change to b does.
Why the dynamic dependencies are correct. The computed’s value depends on the inputs that were actually read. A signal that was not read does not affect the value, so it is not a dependency. The dynamic list is the correct model, and the automatic tracking produces it.
Why the reads must be synchronous. The tracking records the reads during the synchronous execution of the function. A read in a setTimeout, a Promise, or an async function is not tracked because it happens after the computation has finished. The computed’s function must read the signals synchronously.
Why the tracked context is per-computation. Each computation has its own context, and the reads within it are recorded for that computation. A nested computed has its own context, and its reads are recorded for the nested computed, not the outer one. The contexts are separate, and the graph is built from the nested dependencies.
Why the tracking is the foundation of the graph. The dependency list of each computed is the edge set of the graph. The graph is built dynamically as the computeds are read, and it updates when the reads change. The graph is what makes the propagation efficient — only the nodes that depend on the changed signal are marked.
Lazy evaluation and memoization
The computed is lazy and memoized. The computation runs when the value is read, and the result is cached.
The laziness. The computation runs when the value is read, not when a dependency changes. A dependency change marks the computed as stale, but the recomputation is deferred until the next read.
const a = signal(1);
const doubled = computed(() => {
console.log('computing');
return a() * 2;
});
// No computation yet.
a.set(2); // marks stale, no computation
a.set(3); // marks stale, no computation
console.log(doubled()); // computing, 6 (one computation, not three)
The two changes to a mark the computed as stale, but the computation runs only once, on the read. The intermediate values are never computed, and the final read sees the latest value.
Why the laziness is efficient. A computed that is not read does not compute. A computed that is read after several changes computes once with the latest values, not once per change. The laziness is what makes the graph efficient for the values that are only occasionally read.
The memoization. The computed caches the result. If no dependency has changed since the last read, the cached value is returned.
console.log(doubled()); // computing, 6
console.log(doubled()); // 6 (cached, no computing)
The second read returns the cached value. The computation runs once, and the subsequent reads are free.
Why the memoization matters for the template. A template that reads a computed in multiple places — the value is used in several bindings — computes once and reads the cache for the rest. The memoization is what makes the computed cheap in the template.
Why the memoization is not a subscription. The computed does not subscribe to the dependencies. The dependencies are recorded during the computation, and the computed is marked stale when a dependency changes. The marking is a flag, and the recomputation is deferred. The model is not a subscription; it is a dependency graph with lazy evaluation.
Why the caching can be surprising. A computed that reads a signal that has changed but whose value is the same — a signal set to the value it already holds — does not recompute, because the signal’s change is a no-op. The signal’s value is compared by reference, and an identical reference does not trigger the notification.
Why the comparison is by reference. The signal’s value is compared with the new value by reference. A new object with the same contents is a change; the same object is not. The immutable update is what makes the change detection correct.
Why the computed can be expensive. A computed that reads many signals and does a lot of work is expensive when it recomputes. The memoization reduces the frequency, but the first read after a change is the full computation. The expensive computations should be split, or moved to a different mechanism, or the read should be deferred.
Computed vs method
A method that reads signals computes on every call. A computed signal caches the result. The difference is the caching, and the difference matters in a template.
@Component({
template: `
<p>Method: {{ totalMethod() }}</p>
<p>Computed: {{ totalComputed() }}</p>
<p>Method again: {{ totalMethod() }}</p>
`,
})
export class CartComponent {
readonly items = signal<CartItem[]>([]);
totalMethod(): number {
return this.items().reduce((sum, i) => sum + i.price, 0);
}
readonly totalComputed = computed(() =>
this.items().reduce((sum, i) => sum + i.price, 0),
);
}
The totalMethod runs three times — once for each call. The totalComputed runs once and the second call returns the cache. In a template with several reads, the difference is the number of computations.
Why the method is simpler to write. A method is a plain function. A computed is a signal with the tracking and the caching. For a value that is read once, the method is fine, and the computed is unnecessary.
Why the computed is better for the template. A template may read the value in several bindings, and the change detection may run multiple times. The computed computes once and returns the cache for the rest. The efficiency is the reason.
Why the method is wrong for the change detection. A method that reads signals does not register the component as a consumer of the signals. The template’s call to the method is what marks the component, but the method’s reads are not tracked. The component is checked on every change detection cycle, which is the traditional behavior. The computed’s reads are tracked, and the component is checked only when the computed’s dependencies change.
Why the computed is the modern pattern. The Angular team recommends the computed for the derived values in the template. The method is the old pattern, and it produces the traditional change detection. The computed is the newer pattern, and it produces the targeted change detection.
Why the two can coexist. A method can be used for the values that are read once or that do not depend on signals. A computed is for the values that are derived from signals and read in the template. The choice is per value, and the two can be mixed in the same component.
Why the choice matters for performance. A method that is called in the template runs on every change detection. A computed runs only when its dependencies change. In a component with many derived values, the computed is the difference between a fast render and a slow one.
Computed composition
A computed can depend on other computeds, and the graph is transitive. The composition is how a complex derivation is built from small pieces.
const items = signal<CartItem[]>([]);
const subtotal = computed(() =>
items().reduce((sum, i) => sum + i.price * i.quantity, 0),
);
const tax = computed(() => subtotal() * 0.1);
const shipping = computed(() => (subtotal() > 100 ? 0 : 10));
const total = computed(() => subtotal() + tax() + shipping());
const itemCount = computed(() =>
items().reduce((sum, i) => sum + i.quantity, 0),
);
The subtotal depends on items. The tax, shipping, and total depend on subtotal. The total depends on subtotal, tax, and shipping. The itemCount depends on items directly. The graph has one source (items) and five derived nodes.
Why the composition is the point. A complex derivation is built from small, testable pieces. Each computed does one thing, and the pieces compose. The graph is the derivation, and the individual computeds are the steps.
Why the composition is efficient. A change to items marks the subtotal and the itemCount as stale. The subtotal‘s staleness propagates to the tax, shipping, and total. When the template reads the total, the subtotal, the tax, and the shipping recompute in order, and the total returns the new value. The itemCount recomputes when it is read. The propagation is the graph, and the laziness defers the work.
Why the composition can be deep. The graph can be as deep as the derivation requires. A chain of ten computeds is fine, and the propagation is linear. The depth is a design choice, and the pieces should be as small as the derivation needs.
Why the composition should avoid redundancy. A computed that recomputes a value that another computed already computed is a waste. The shared value should be a computed that both depend on. The subtotal in the example is the shared value, and the tax, shipping, and total depend on it.
Why the composition is testable. Each computed is a function that reads signals and returns a value. The function can be tested in isolation, with the signals set to the desired values. The composition is the graph, and the individual nodes are the tests.
Why the composition can have a diamond. A diamond in the graph — two paths from a source to a computed — is fine. The total depends on subtotal directly and through tax and shipping. The diamond is the graph, and the consistency is guaranteed.
Why the diamond is glitch-free. The graph ensures that the total sees the subtotal, the tax, and the shipping at the same revision. The propagation is topological, and the values are consistent. The glitch-free property is what makes the derived values reliable.
Read-only and the graph
A computed is read-only. The value is derived, and the only way to change it is to change its dependencies. The read-only nature is what makes the graph acyclic and the propagation reliable.
const a = signal(1);
const b = computed(() => a() * 2);
// b.set(10); // ❌ Property 'set' does not exist on type 'Signal<number>'
The compiler rejects the set call. The computed’s value is derived, and the write is not allowed.
Why the read-only is a safety feature. A computed that could be written would break the derivation. The value would no longer be a function of the dependencies, and the graph would be inconsistent. The read-only nature is what keeps the model coherent.
Why the computed is typed as Signal<T>. The computed function returns a Signal<T>, which is the read-only interface. The WritableSignal<T> is the interface with set and update, and the signal function returns it. The two interfaces are separate, and the computed returns the read-only one.
Why the read-only can be exposed. A service can expose a computed as the public API, and the consumers can read it but not write it. The writable signal stays private, and the derived value is the public surface.
@Injectable({ providedIn: 'root' })
export class CartService {
private readonly items = signal<CartItem[]>([]);
readonly total = computed(() => this.items().reduce((s, i) => s + i.price, 0));
add(item: CartItem): void {
this.items.update((list) => [...list, item]);
}
}
The total is the public read-only value, and the add method is the write. The writable signal is private, and the consumers cannot mutate the state directly.
Why the graph is acyclic. A computed depends on other signals. A cycle — a computed that depends on itself, directly or transitively — would be an infinite loop. The model does not allow the cycle, and the read-only nature is what prevents the write that would create one. The graph is a DAG, and the propagation is finite.
Why the acyclicity is guaranteed by the API. A computed cannot be written, so a computed cannot be a dependency of itself through a write. The only way a cycle could form is if a signal were written inside a computed that reads the signal, which is a side effect and is discouraged. The API prevents the natural cycle, and the discipline prevents the unnatural one.
Why the read-only graph is the model. The signals are the sources, the computeds are the derived, and the flow is one direction. The model is the graph, and the read-only nature is what makes the graph a DAG. The consistency and the efficiency follow from the DAG.
Common pitfalls
The computed is simple, but the patterns have pitfalls.
The side effect in the computation. A computed’s function should be pure. A side effect — a log, a DOM update, a fetch — runs whenever the computed recomputes, which is unpredictable. The side effect belongs in an effect.
// Wrong
const doubled = computed(() => {
console.log('computing');
return a() * 2;
});
// Right
const doubled = computed(() => a() * 2);
effect(() => console.log('doubled is', doubled()));
The write to a signal the computed reads. A computed that writes to a signal it reads creates a loop. The write triggers the signal’s notification, which marks the computed as stale, which recomputes, which writes again. The write belongs in an effect or a method, and the computed should be pure.
// Wrong
const doubled = computed(() => {
const value = a() * 2;
b.set(value); // ❌ write in a computed
return value;
});
The async read in the computation. A computed’s function must read the signals synchronously. A read in a setTimeout, a Promise, or an async function is not tracked, and the dependency is missed.
// Wrong
const value = computed(async () => {
await something();
return a(); // ❌ the read is not tracked
});
The computed that reads a signal it should not. A computed that reads a signal that is not part of its derivation is a hidden dependency. The computed recomputes when the unrelated signal changes, which is a waste. The reads should be the derivation, and nothing else.
The computed that is not read. A computed that is never read does not compute, and its dependencies are not tracked. A computed that is expected to run — a side effect disguised as a computed — does not run. The computed is lazy, and the laziness is the model.
The computed that shadows a signal. A computed with the same name as a signal is a mistake, and the shadowing produces confusion. The names should be distinct, and the computed’s name should describe the derived value.
The computed that does too much. A computed that reads ten signals and does a lot of work is expensive when it recomputes. The computed should be split into smaller pieces, and the pieces should compose. The small pieces are testable, and the composition is efficient.
Why the pitfalls are about the purity and the tracking. The computed’s model is the pure function and the tracked reads. A side effect breaks the purity, and an async read breaks the tracking. The two rules — pure and synchronous — are what keep the computed correct.
Complete Example Session
import { Component, Injectable, signal, computed, effect } from '@angular/core';
// ============================================
// PART 1: THE BASIC COMPUTED
// ============================================
const price = signal(100);
const quantity = signal(2);
const subtotal = computed(() => price() * quantity());
console.log(subtotal()); // 200
price.set(150);
console.log(subtotal()); // 300
// ============================================
// PART 2: THE LAZY AND MEMOIZED COMPUTED
// ============================================
const a = signal(1);
const doubled = computed(() => {
console.log('computing');
return a() * 2;
});
console.log(doubled()); // computing, 2
console.log(doubled()); // 2 (cached)
a.set(5);
console.log(doubled()); // computing, 10
// ============================================
// PART 3: THE DYNAMIC DEPENDENCY
// ============================================
const useA = signal(true);
const x = signal(1);
const y = signal(2);
const value = computed(() => (useA() ? x() : y()));
console.log(value()); // 1
useA.set(false);
console.log(value()); // 2
// ============================================
// PART 4: THE COMPOSITION
// ============================================
interface CartItem {
id: string;
price: number;
quantity: number;
}
const items = signal<CartItem[]>([]);
const subtotal2 = computed(() =>
items().reduce((sum, i) => sum + i.price * i.quantity, 0),
);
const tax = computed(() => subtotal2() * 0.1);
const shipping = computed(() => (subtotal2() > 100 ? 0 : 10));
const total = computed(() => subtotal2() + tax() + shipping());
const itemCount = computed(() =>
items().reduce((sum, i) => sum + i.quantity, 0),
);
items.set([{ id: '1', price: 50, quantity: 2 }]);
console.log(subtotal2()); // 100
console.log(tax()); // 10
console.log(shipping()); // 10
console.log(total()); // 120
// ============================================
// PART 5: THE COMPONENT
// ============================================
@Component({
selector: 'app-cart',
standalone: true,
template: `
<p>Items: {{ itemCount() }}</p>
<p>Subtotal: {{ subtotal() | currency }}</p>
<p>Tax: {{ tax() | currency }}</p>
<p>Shipping: {{ shipping() | currency }}</p>
<p>Total: {{ total() | currency }}</p>
`,
})
export class CartComponent {
private readonly cart = inject(CartService);
readonly items = this.cart.items;
readonly itemCount = this.cart.itemCount;
readonly subtotal = this.cart.subtotal;
readonly tax = this.cart.tax;
readonly shipping = this.cart.shipping;
readonly total = this.cart.total;
}
// ============================================
// PART 6: THE SERVICE
// ============================================
@Injectable({ providedIn: 'root' })
export class CartService {
private readonly itemsSignal = signal<CartItem[]>([]);
readonly items = this.itemsSignal.asReadonly();
readonly subtotal = computed(() =>
this.itemsSignal().reduce((sum, i) => sum + i.price * i.quantity, 0),
);
readonly tax = computed(() => this.subtotal() * 0.1);
readonly shipping = computed(() => (this.subtotal() > 100 ? 0 : 10));
readonly total = computed(() => this.subtotal() + this.tax() + this.shipping());
readonly itemCount = computed(() =>
this.itemsSignal().reduce((sum, i) => sum + i.quantity, 0),
);
add(item: CartItem): void {
this.itemsSignal.update((list) => [...list, item]);
}
remove(id: string): void {
this.itemsSignal.update((list) => list.filter((i) => i.id !== id));
}
}
// ============================================
// PART 7: THE COMPUTED FOR THE FILTERED LIST
// ============================================
@Component({ selector: 'app-search', standalone: true, template: `` })
export class SearchComponent {
readonly term = signal('');
readonly items = signal<Item[]>([]);
readonly filtered = computed(() => {
const term = this.term().toLowerCase();
if (!term) return this.items();
return this.items().filter((i) => i.name.toLowerCase().includes(term));
});
}
// ============================================
// PART 8: THE COMPUTED FOR THE GROUPING
// ============================================
const grouped = computed(() => {
const map = new Map<string, Item[]>();
for (const item of items()) {
const key = item.category;
if (!map.has(key)) map.set(key, []);
map.get(key)!.push(item);
}
return map;
});
// ============================================
// PART 9: THE COMPUTED VS THE METHOD
// ============================================
// Method: recomputes on every call
totalMethod(): number {
return this.items().reduce((sum, i) => sum + i.price, 0);
}
// Computed: recomputes when the dependencies change
readonly totalComputed = computed(() =>
this.items().reduce((sum, i) => sum + i.price, 0),
);
// ============================================
// PART 10: WHAT NOT TO DO
// ============================================
// Don't put a side effect in a computed
computed(() => { console.log('x'); return a() * 2; }); // impure
// Don't write to a signal in a computed
computed(() => { b.set(a() * 2); return a() * 2; }); // loop
// Don't read a signal asynchronously in a computed
computed(async () => { await x(); return a(); }); // not tracked
// Don't expect a computed to run without a read
// The computed is lazy.
// Don't use a computed for a value that does not depend on signals
// Use a method or a constant.
// Don't over-compose
// A chain of ten computeds for a simple derivation is unnecessary.
The ten parts cover the basic computed, the lazy and memoized behavior, the dynamic dependency, the composition, the component, the service, the filtered list, the grouping, the computed vs the method, and the anti-patterns.
Quick Reference
The computed Function
| Form | Purpose |
|---|---|
computed(() => expr) | Basic derived value |
computed(() => { ... return x; }) | Multi-line computation |
The Read API
| Operation | Result |
|---|---|
comp() | The derived value |
comp in the template | The value, tracked |
comp in another computed | The value, tracked |
comp in an effect | The value, tracked |
The Write API
| Operation | Result |
|---|---|
comp.set(...) | ❌ does not exist |
comp.update(...) | ❌ does not exist |
The Model
| Property | Value |
|---|---|
| Read-only | ✅ |
| Lazy | ✅ |
| Memoized | ✅ |
| Tracked | Automatic |
| Pure | Required |
| Synchronous | Required |
Computed vs Method
| Aspect | Method | Computed |
|---|---|---|
| Recomputes | Every call | On dependency change |
| Cached | No | Yes |
| Tracked | No | Yes |
| Change detection | Traditional | Targeted |
| Use | Read once | Read in template |
Common Derivations
| Derivation | Code |
|---|---|
| Sum | computed(() => items().reduce((s, i) => s + i.price, 0)) |
| Filter | computed(() => items().filter((i) => i.active)) |
| Group | computed(() => groupBy(items(), 'category')) |
| Count | computed(() => items().length) |
| Derived boolean | computed(() => items().length > 0) |
Best Practices
✅ Do This:
// Use computed for the derived value
readonly total = computed(() => this.items().reduce((s, i) => s + i.price, 0)); // ✅
// Use the composition for the complex derivation
const subtotal = computed(() => ...);
const tax = computed(() => subtotal() * 0.1);
const total = computed(() => subtotal() + tax()); // ✅
// Keep the computed pure
const doubled = computed(() => this.count() * 2); // ✅
// Read the signals synchronously
const value = computed(() => this.a() + this.b()); // ✅
// Use asReadonly for the public API
readonly items = this.items.asReadonly(); // ✅
// Use the computed in the template
template: `{{ total() }}` // ✅
// Split the expensive computation
const filtered = computed(() => this.items().filter(...));
const sorted = computed(() => [...filtered()].sort(...)); // ✅
❌ Don’t Do This:
// Don't put a side effect in a computed
computed(() => { console.log('x'); return this.a() * 2; }); // ⚠️
// Don't write to a signal in a computed
computed(() => { this.b.set(this.a() * 2); return this.a() * 2; }); // ⚠️
// Don't read a signal asynchronously
computed(async () => { await x(); return this.a(); }); // ⚠️
// Don't use a computed for a value without dependencies
computed(() => 42); // use a constant // ⚠️
// Don't expect a computed to run without a read
// The computed is lazy. // ⚠️
// Don't use a method for a value read in the template
totalMethod(): number { return this.items().reduce(...); } // ⚠️
// Don't over-compose
const a = computed(() => this.x() + 1);
const b = computed(() => a() + 1);
const c = computed(() => b() + 1);
// A single computed would suffice for a simple derivation.
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
| Side effect in computed | Impure | Move to effect |
| Write in computed | Loop | Move to a method |
| Async read | Not tracked | Synchronous reads |
Missing () | Reads the function | Call the signal |
| Computed not read | Does not run | Read it, or use an effect |
| Over-composition | Unnecessary layers | Simplify |
| Method for template value | Recomputes every call | Use a computed |
| Shared value not extracted | Redundant computation | Extract a computed |
Real-World Examples
1. Cart total
readonly total = computed(() => this.items().reduce((s, i) => s + i.price, 0));
2. Tax
readonly tax = computed(() => this.subtotal() * 0.1);
3. Shipping
readonly shipping = computed(() => (this.subtotal() > 100 ? 0 : 10));
4. Item count
readonly itemCount = computed(() => this.items().reduce((s, i) => s + i.quantity, 0));
5. Filtered list
readonly filtered = computed(() => this.items().filter((i) => i.active));
6. Search
readonly results = computed(() => this.items().filter((i) => i.name.includes(this.term())));
7. Derived boolean
readonly isEmpty = computed(() => this.items().length === 0);
8. Grouped
readonly grouped = computed(() => groupBy(this.items(), 'category'));
9. Selected item
readonly selected = computed(() => this.items().find((i) => i.id === this.selectedId()));
10. Formatted value
readonly formattedTotal = computed(() => `$${this.total().toFixed(2)}`);
Visual: The Computed
┌──────────────────────────────────────────────────────────┐
│ const price = signal(100) │
│ const quantity = signal(2) │
│ │ │
│ ▼ │
│ const subtotal = computed(() => price() * quantity()) │
│ │
│ subtotal() → 200 │
│ price.set(150) │
│ subtotal() → 300 │
│ │
│ The computed tracks the reads. │
│ The reads are price and quantity. │
│ │
└──────────────────────────────────────────────────────────┘
Visual: Lazy and Memoized
┌──────────────────────────────────────────────────────────┐
│ const a = signal(1) │
│ const doubled = computed(() => a() * 2) │
│ │
│ a.set(2) → marks stale, no computation │
│ a.set(3) → marks stale, no computation │
│ a.set(4) → marks stale, no computation │
│ │
│ doubled() → computes once, returns 8 │
│ │
│ doubled() → returns 8 (cached) │
│ │
│ The computation is lazy and memoized. │
│ The intermediate values are never computed. │
│ │
└──────────────────────────────────────────────────────────┘
Visual: The Dependency Graph
┌──────────────────────────────────────────────────────────┐
│ items: signal │
│ │ │
│ ├──────────────────────────┐ │
│ ▼ ▼ │
│ subtotal: computed itemCount: computed │
│ │ │
│ ├──────────┬──────────┐ │
│ ▼ ▼ ▼ │
│ tax: computed shipping: computed │
│ │ │ │
│ └────┬─────┘ │
│ ▼ │
│ total: computed │
│ │
│ The graph is a DAG. │
│ The propagation is topological. │
│ The values are consistent. │
│ │
└──────────────────────────────────────────────────────────┘
Visual: The Dynamic Dependency
┌──────────────────────────────────────────────────────────┐
│ const useA = signal(true) │
│ const a = signal(1) │
│ const b = signal(2) │
│ │
│ const value = computed(() => useA() ? a() : b()) │
│ │
│ value() → 1 │
│ Dependencies: { useA, a } │
│ │
│ useA.set(false) │
│ value() → 2 │
│ Dependencies: { useA, b } │
│ │
│ a.set(10) → no recomputation (a is not a dependency) │
│ b.set(20) → recomputation (b is a dependency) │
│ │
│ The dependency list is dynamic. │
│ The reads are the dependencies. │
│ │
└──────────────────────────────────────────────────────────┘
Visual: Computed vs Method
┌──────────────────────────────────────────────────────────┐
│ METHOD │
│ │
│ totalMethod() { │
│ return this.items().reduce(...); │
│ } │
│ │
│ Template: {{ totalMethod() }} {{ totalMethod() }} │
│ │ │
│ └── runs twice │
│ │
│ Change detection: checks the component on every cycle │
│ │
├──────────────────────────────────────────────────────────┤
│ COMPUTED │
│ │
│ total = computed(() => this.items().reduce(...)); │
│ │
│ Template: {{ total() }} {{ total() }} │
│ │ │
│ └── computes once, reads the cache │
│ │
│ Change detection: checks the component when the │
│ dependencies change │
│ │
└──────────────────────────────────────────────────────────┘
Visual: The Composition
┌──────────────────────────────────────────────────────────┐
│ items: signal │
│ │ │
│ ▼ │
│ subtotal = computed(() => items().reduce(...)) │
│ │ │
│ ├──► tax = computed(() => subtotal() * 0.1) │
│ │ │
│ ├──► shipping = computed(() => subtotal() > 100 ? 0 : 10)│
│ │ │
│ └──► total = computed(() => subtotal() + tax() + shipping())│
│ │
│ items.set([...]) │
│ │ │
│ ▼ │
│ subtotal recomputes │
│ │ │
│ ├──► tax recomputes │
│ ├──► shipping recomputes │
│ └──► total recomputes │
│ │
│ The propagation is the graph. │
│ The recomputation is lazy. │
│ │
└──────────────────────────────────────────────────────────┘
Summary
| Property | Value |
|---|---|
| Created with | computed(fn) |
| Read | comp() |
| Write | Not allowed |
| Dependencies | The signals read |
| Recompute | When a dependency changes |
| Evaluation | Lazy |
| Caching | Memoized |
| Purity | Required |
| Synchronous | Required |
| Return type | Signal<T> |
| Comparison | Method | Computed |
|---|---|---|
| Recompute | Every call | On dependency change |
| Cache | No | Yes |
| Tracking | No | Yes |
| Change detection | Traditional | Targeted |
Key takeaways:
- A computed signal is a read-only derived value — it reads other signals and produces a value from them, and it cannot be set
- The dependencies are the signals the function reads — the reads are recorded automatically, and the dependency list is dynamic
- The computation is lazy — the function runs when the value is read, not when a dependency changes
- The result is memoized — the cached value is returned if no dependency has changed since the last read
- The function must be pure and synchronous — a side effect or an async read breaks the model
- Computed values can depend on other computed values — the graph is transitive, and the composition is the point
- The graph is a DAG — the read-only nature prevents the cycles, and the propagation is topological and glitch-free
- The computed is the modern replacement for a
getteror a method in the template — the caching and the tracking are the improvements - The computed is the modern replacement for a
combineLatestofBehaviorSubjects — the signal version is synchronous and subscription-free - The expensive computation should be split — the small computeds are testable, and the composition is efficient
Remember: A computed signal is a derived value that tracks its dependencies and caches its result. The reads are the dependencies, the computation is lazy, and the result is memoized. The function must be pure and synchronous, and the composition is the graph. The computed is the modern answer for the derived state, and it is the foundation of the signal-based change detection. Use it for the values that are derived from signals and read in the template, and use a method only for the values that do not depend on signals.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!