Signals are the foundation of modern Angular reactivity and change detection.
A signal is a wrapper around a value that notifies interested consumers when it changes.
count = signal(0); // WritableSignal<number>
count(); // read: call it like a function
count.set(5); // replace the value
count.update((c) => c + 1); // compute from the previous value`computed` creates a read-only signal derived from others:
double = computed(() => this.count() * 2);Computed signals are lazy and memoised: the function runs only when the value is read after one of its dependencies changed. Dependencies are tracked automatically from the signals read during the last run, so conditional branches create dynamic dependencies.
When a template reads a signal, Angular knows exactly which component to refresh when it changes. That is what makes zoneless, OnPush-by-default Angular work.
Rules that prevent bugs:
- Don't mutate values in place.
this.items().push(x)changes the array without notifying anyone. Useupdatewith a new array or object:items.update((list) => [...list, x]). - Signals compare with
Object.isby default; setting an equal value does nothing. Pass a customequaloption if needed. - Keep computed functions pure: no setting other signals and no side effects inside them.
- Expose read-only views with
asReadonly()so only the owner can write.
Example
import { Component, signal, computed } from '@angular/core';
type Item = { name: string; price: number };
@Component({
selector: 'app-cart',
template: `
<p>{{ items().length }} items, total {{ total() }}</p>
<button (click)="add()">Add</button>
<button (click)="discount.set(0.1)">10% off</button>
`,
})
export class Cart {
protected items = signal<Item[]>([{ name: 'Pen', price: 2 }]);
protected discount = signal(0);
protected total = computed(() => {
const sum = this.items().reduce((s, i) => s + i.price, 0);
return Math.round(sum * (1 - this.discount()) * 100) / 100;
});
protected add() {
// this.items().push(...) would NOT update the view
this.items.update((list) => [...list, { name: 'Pad', price: 5 }]);
}
}Remember
- Read a signal by calling it; write with
setorupdate computedis lazy, memoised and tracks dependencies automatically- Update arrays and objects immutably; in-place mutation notifies nobody
Watch out for
- Mutating an array or object inside a signal and expecting the UI to update.