The writable signal: reading and writing
Key takeaway — You read a signal by calling it
count(), you replace its value withcount.set(x), and you derive it from the previous one withcount.update(n => n + 1). The template updates on its own.
The writable signal is the building block. It is a value you can read and modify, and that the interface reacts to automatically. This chapter covers its creation, its three operations (set, update, reading) and its display in a component.
How do you create and read a signal?
You create a signal with the signal() function, passing it an initial value. The type is inferred, but you can specify it:
import { signal } from '@angular/core';
const count = signal(0); // WritableSignal<number>
const nom = signal<string>('Ada'); // explicit type
const actif = signal(false); // WritableSignal<boolean>
To read the value, you call the signal like a function:
console.log(count()); // 0
console.log(nom()); // 'Ada'
This
count()call is not just a read: it registers a dependency. If the read happens inside a template or acomputed, that consumer will be notified on the next change.
set() or update(): what is the difference?
There are two ways to write to a writable signal.
set(value) replaces the value, regardless of the previous one:
count.set(10); // the value becomes 10
update(fn) computes the new value from the previous one:
count.update((n) => n + 1); // increments: 10 → 11
Rule of thumb: use set when the new value is independent of the current one (resetting to 0, assigning an input), and update when it depends on it (incrementing, toggling a boolean, adding to a list).
actif.update((v) => !v); // toggles true/false
liste.update((arr) => [...arr, nouvel]); // adds without mutating the old array
Immutability, a point to watch
A signal only detects a change if you provide it a new reference. Mutating in place triggers no update:
const panier = signal<string[]>(['café']);
// ❌ DOES NOT WORK: we mutate the existing array, the reference does not change.
panier().push('thé');
// ✅ CORRECT: we create a new array.
panier.update((items) => [...items, 'thé']);
The same rule applies to objects: replace via { ...ancien, champ: valeur } rather than assigning a property.
Reading a signal in a template
In a component, you simply call the signal in the template. Angular tracks the dependency and refreshes the display on every change — including with the OnPush strategy, strongly recommended with signals.
import { Component, signal, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-root',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<h1>Counter: {{ count() }}</h1>
<button (click)="decrement()">−</button>
<button (click)="increment()">+</button>
<button (click)="reset()">Reset</button>
<p>Doubled value (computed in the template): {{ count() * 2 }}</p>
`,
})
export class AppComponent {
readonly count = signal(0);
increment(): void {
this.count.update((n) => n + 1);
}
decrement(): void {
this.count.update((n) => n - 1);
}
reset(): void {
this.count.set(0);
}
}
Note the readonly on the property: the signal's reference never changes (it is its internal value that evolves), so we protect the field against accidental reassignment.
Try it live
Edit the code below (for example, make it increment by 5) and watch the instant update:
signal-basics
Read-only signal: asReadonly()
To expose a signal without allowing writes from the outside (useful for a service), use asReadonly():
private readonly _count = signal(0);
readonly count = this._count.asReadonly(); // Signal<number>, without set/update
increment(): void {
this._count.update((n) => n + 1); // writing stays internal
}
We will see this pattern again in chapter 9 to build a clean state service.
Equality and ignored changes
By default, a signal compares the old and new values with Object.is. If they are identical, no consumer is notified:
const x = signal(3);
x.set(3); // same value → no refresh triggered
For objects, you can provide a custom equality function:
const user = signal(
{ id: 1, nom: 'Ada' },
{ equal: (a, b) => a.id === b.id }, // treats as "equal" if same id
);
Key points
signal(value)creates a reactive source; you read it by calling it.setreplaces,updatederives from the previous value.- Always provide a new reference for arrays and objects.
asReadonly()protects against writes; theequaloption controls detection.
In the next chapter, we move on to derived values with computed(): how to automatically compute a total, a filter or a label from other signals.