Angular Signals : vers un Angular sans Zone.js
· 7 min de lecture
Les signals changent la façon dont Angular met l’écran à jour. Voici comment ils marchent, et comment migrer une application existante.
En bref
Angular met l’écran à jour quand les données changent. Avec les signals, il sait exactement quoi mettre à jour, au lieu de tout vérifier. L’application devient plus rapide et plus facile à suivre.
Un mot technique vous échappe ? Le glossaire du développement web l’explique simplement.
Jusqu’ici, Angular utilise Zone.js pour savoir quand mettre l’écran à jour. Chaque événement, minuteur ou requête HTTP lance une vérification de tous les composants. C’est simple, mais coûteux : sur une grosse application, ces vérifications rendent l’interface moins réactive (voir « INP : optimiser la réactivité perçue de son site »).
Une réactivité fine
Un signal est une valeur qui notifie ses consommateurs quand elle change. Angular sait exactement quels templates en dépendent et ne met à jour que ceux-là.
@Component({
selector: 'app-cart',
template: `<p>Total: {{ total() }} €</p>`,
})
export class CartComponent {
items = signal<Item[]>([]);
total = computed(() =>
this.items().reduce((sum, i) => sum + i.price, 0)
);
add(item: Item) {
this.items.update(list => [...list, item]);
}
}Migrer pas à pas
- Remplacer les @Input() par input() et les @Output() par output().
- Convertir les BehaviorSubject d’état local en signal().
- Utiliser toSignal() pour consommer les Observables existants (HttpClient).
- Activer provideZonelessChangeDetection() une fois les composants en OnPush.
signal, computed et effect
- signal() crée une valeur que l’on peut modifier. On la lit en l’appelant comme une fonction. On la modifie avec set() ou update().
- computed() dérive une valeur d’autres signals. Elle n’est recalculée que si l’une de ses dépendances a changé, et seulement quand on la lit.
- effect() lance une action à chaque changement des signals qu’il lit. Par exemple : mettre à jour le localStorage, écrire dans la console ou piloter une librairie externe.
Un effect ne doit pas servir à recopier un signal dans un autre : c’est le rôle de computed(). Réserver les effects aux échanges avec le monde extérieur garde un flux de données lisible.
@Injectable({ providedIn: 'root' })
export class ThemeService {
readonly colorScheme = signal<'light' | 'dark'>('light');
constructor() {
effect(() => {
document.documentElement.dataset['colorScheme'] = this.colorScheme();
});
}
}Des composants entièrement réactifs
Les entrées et sorties des composants ont aussi leur version signal. Un input() se lit comme un signal et peut donc alimenter un computed(), sans ngOnChanges ni setter. model() gère la liaison bidirectionnelle, et output() remplace EventEmitter.
export class ProductCardComponent {
product = input.required<Product>();
quantity = model(1);
added = output<Product>();
total = computed(() => this.product().price * this.quantity());
}Les pièges à éviter
- Muter un tableau ou un objet en place : un signal compare les valeurs par référence, la vue ne serait pas notifiée. Toujours produire une nouvelle valeur avec update().
- Changer un signal dans un computed() : Angular ne le permet pas. Un computed doit rester une fonction pure.
- Multiplier les effects : chacun est une source de mises à jour difficile à suivre. Un computed() suffit dans la plupart des cas.
Sans Zone.js, qui déclenche le rendu ?
En mode zoneless, Angular ne surveille plus les événements du navigateur. L’écran se met à jour dans quatre cas : un signal lu dans un template change, un événement écouté dans un template se produit, l’AsyncPipe reçoit une valeur, ou on appelle markForCheck(). Sans Zone.js, le bundle est plus léger. Les stack traces sont aussi plus lisibles, car Angular ne modifie plus les API du navigateur.
RxJS ne disparaît pas : il reste l’outil idéal pour les flux asynchrones complexes. Les signals, eux, deviennent la façon naturelle de représenter l’état.