Signals, computed values and effects
Use signal, computed, linkedSignal and effect for the state that drives a template, and know when an observable is still the better tool.
signal and computed
import { Component, signal, computed, linkedSignal } from '@angular/core';
@Component({
selector: 'app-cart',
template: `
<p>Items: {{ count() }}</p>
<p>Subtotal: {{ subtotal() | currency }}</p>
<p>Shipping: {{ shippingLabel() }}</p>
<button (click)="add({ id: 1, name: 'Widget', price: 1250 })">Add widget</button>
<button (click)="reset()">Reset</button>
`
})
export class CartComponent {
// writable state
readonly items = signal<CartItem[]>([]);
readonly promoCode = signal<string | null>(null);
// derived state: recomputed lazily, cached until a dependency changes
readonly subtotal = computed(() =>
this.items().reduce((sum, item) => sum + item.price, 0)
);
readonly discount = computed(() => (this.promoCode() ? this.subtotal() * 0.1 : 0));
readonly total = computed(() => this.subtotal() - this.discount());
// derived from a signal but also writable: resets whenever the source changes
readonly shippingLabel = linkedSignal({
source: this.total,
computation: (total) => (total > 5000 ? 'Free' : 'Flat rate')
});
add(item: CartItem) { this.items.update((list) => [...list, item]); }
reset() { this.items.set([]); this.promoCode.set(null); }
}| API | Meaning | Notes |
|---|---|---|
signal(v) | Writable state | .set, .update, call it to read |
computed(fn) | Derived, read-only | Lazy and memoised; no side effects |
linkedSignal({source, computation}) | Derived but writable | Re-runs its computation when the source changes |
effect(fn) | Side effect on dependency change | Runs after render; not for derived state |
untracked(fn) | Read without subscribing | For values you do not want to depend on |
input() / model() | Signal inputs and two-way bindings | Replaces @Input and @Output pairs |
💡
A
computed must be pure. If you find yourself writing to another signal inside one, the graph will fight you: use linkedSignal for derived-but-writable state, or move the work into an effect whose purpose is explicitly a side effect.Effects and cleanup
import { Component, signal, effect, inject, Injector, untracked } from '@angular/core';
import { DOCUMENT } from '@angular/common';
@Component({ selector: 'app-theme', template: `<button (click)="toggle()">Theme</button>` })
export class ThemeComponent {
private readonly doc = inject(DOCUMENT);
private readonly injector = inject(Injector);
readonly mode = signal<'light' | 'dark'>('light');
constructor() {
// Effect body runs whenever a signal it reads changes.
// An effect is auto-destroyed with its injection context; onDestroy cleans up.
effect((onCleanup) => {
const mode = this.mode();
const root = this.doc.documentElement;
root.setAttribute('data-theme', mode);
// Cleanup runs before the next run and when the component is destroyed.
onCleanup(() => root.removeAttribute('data-theme'));
});
// Reading a signal inside untracked() does NOT make it a dependency.
effect(() => {
const mode = this.mode();
this.logUntracked(mode, untracked(() => this.previousMode()));
});
}
private readonly previousMode = signal('light');
private logUntracked(next: string, previous: string) { console.log(previous, '->', next); }
toggle() { this.mode.update((m) => (m === 'light' ? 'dark' : 'light')); }
}effectis for synchronising with something outside Angular: storage, a canvas, the document, an analytics call. It is not a place to compute state.- Effects run after change detection, not before, so the DOM they read already reflects the current render.
allowSignalWritesis a last resort. If an effect writes a signal another effect reads, you have built a cycle — restructure instead.- To create an effect outside a constructor, pass an injector explicitly:
effect(fn, { injector }). - An effect that fires on every keystroke is usually a sign the value should be an input to a service method with its own cancellation, not a reactive side effect.
Interop with observables and RxJS
import { Component, signal, computed } from '@angular/core';
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
import { of } from 'rxjs';
@Component({
selector: 'app-search',
template: `
<input [value]="query()" (input)="query.set($any($event.target).value)" />
<p>{{ results().length }} results</p>
`
})
export class SearchComponent {
private readonly api = inject(CatalogService);
readonly query = signal('');
// signal -> observable, then back to a signal, with real operators in between
private readonly query$ = toObservable(this.query);
readonly results = toSignal(
this.query$.pipe(
debounceTime(250),
distinctUntilChanged(),
switchMap((q) => (q.length < 2 ? of([]) : this.api.search(q)))
),
{ initialValue: [] as Product[] }
);
readonly hasResults = computed(() => this.results().length > 0);
}| Need | Reach for | Why |
|---|---|---|
| A value a template renders | signal | Cheapest reads, integrates with change detection |
| A value derived from other values | computed | Memoised; recalculates only when inputs change |
| Debounce, retry, cancellation | RxJS operators | Signals have no operator vocabulary |
| A stream of events over time | Observable | Rich composition, backpressure semantics |
| Both | toSignal / toObservable | Convert at the boundary, keep one model per layer |
| An HTTP GET bound to a parameter | httpResource / resource | Built-in loading, error and reload states |
The practical rule: signals for state that lives in the component, RxJS for anything with timing semantics — debouncing, cancellation, retries, combining streams. Convert at the edges with toSignal so the template never has to unwrap an observable.
FAQ
When should I use effect instead of computed?
Never for deriving state. Use
computed when the output is a value the template will render, and effect when the output is an action that must happen — a log, a storage write, a DOM call. If the effect result is read by the template, it should have been a computed.Do I still need RxJS in a signal-based app?
Yes, for anything involving time: debouncing a search box, cancelling an in-flight request, retrying with backoff, or merging several streams. Signals model values, not event streams, and the operator library remains the right tool for the latter.
Related
Template control flow, pipes and deferred views RxJS essentials for Angular
Last refreshed 2026-09-18.