Angular Signals : comprendre la réactivité moderne d'Angular
Le tournant majeur dans le développement Angular avec l'arrivée des Signals.
Auteur : Etienne PERIANAYAGASSAMY
Le paysage du développement Angular a connu un tournant majeur avec l'arrivée des Signals. Si vous venez du monde React (Hooks) ou Vue (Composition API), ce concept vous semblera familier. Pour les puristes d'Angular, c'est un changement de paradigme important — non pas une rupture, mais une évolution pensée pour coexister avec l'existant.
1. Avant les Signals : RxJS, un outil puissant… mais parfois surdimensionné
Pendant des années, la gestion de l'état dans Angular reposait presque exclusivement sur RxJS. Pour rendre une application réactive, nous utilisions des BehaviorSubject, des Observable et le fameux pipe async.
RxJS reste irremplaçable pour l'asynchronisme complexe — gestion de flux, debounce, combinaison de requêtes HTTP, etc. Mais pour une simple gestion d'état local, il s'avère souvent surdimensionné :
- Verbosité : déclarer un
BehaviorSubject, l'exposer enObservable, gérer lessubscribeetunsubscribepour afficher un simple compteur, c'est beaucoup de cérémonie. - Zone.js : Angular devait vérifier l'intégralité de l'arbre des composants pour détecter les changements, ce qui pouvait impacter les performances sur de grosses applications.
C'est précisément pour combler ces cas d'usage que les Signals ont été introduits.
2. L'apparition des Signals : Une transition progressive
Les Signals ont été introduits de manière progressive pour garantir une transition fluide avec l'écosystème existant :
- Angular v16 (Mai 2023) : Les Signals font leur entrée en version Developer Preview. La communauté peut commencer à expérimenter la nouvelle réactivité.
- Angular v17 (Novembre 2023) : Les Signals passent en version stable, prêts pour la production. Cette version introduit également une nouvelle syntaxe de template (
@if,@for) — deux features distinctes des Signals, mais qui font partie du même effort de modernisation du framework et s'intègrent naturellement dans un contexte Zoneless.
Leur atout principal : une réactivité granulaire. Contrairement à Zone.js, un Signal sait exactement quel composant — ou quelle partie du template — a besoin d'être mis à jour.
Ce que cela change concrètement :
- Moins de Zone.js à terme : Angular pourra cibler uniquement le "nœud" précis qui a changé, sans parcourir tout l'arbre.
- Simplicité : plus besoin de
subscribeni de gérer les fuites mémoire avecunsubscribe. Un Signal se lit simplement comme une fonction :monSignal().
3. La boîte à outils : Méthodes essentielles
Angular met à disposition plusieurs primitives pour manipuler les Signals :
signal(initialValue): Crée un Signal de base (lecture/écriture).set(newValue): Remplace la valeur du Signal.update(fn): Met à jour la valeur en fonction de la précédente. Pour les tableaux et objets, on utilise le spread plutôt que la mutation directe :items.update(arr => [...arr, newItem]).computed(() => ...): Crée un Signal dérivé, en lecture seule, qui se met à jour automatiquement.effect(() => ...): Exécute une fonction à chaque changement des Signals qu'elle contient. Réservé aux effets de bord.toSignal(observable$): Convertit un Observable RxJS en Signal. Disponible depuis@angular/core/rxjs-interopdès Angular 16 — l'outil clé pour une migration progressive.
Exemple complet : un compteur réactif
import { Component, signal, computed, effect } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<p>Compteur : {{ count() }}</p>
<p>Double : {{ double() }}</p>
<button (click)="increment()">+1</button>
<button (click)="reset()">Reset</button>
`
})
export class CounterComponent {
// Signal de base
count = signal(0);
// Signal dérivé (lecture seule, recalculé automatiquement)
double = computed(() => this.count() * 2);
constructor() {
// Effet de bord : s'exécute à chaque changement de count()
effect(() => {
console.log('Nouvelle valeur :', this.count());
});
}
increment() {
this.count.update(c => c + 1);
}
reset() {
this.count.set(0);
}
}
Exemple avec toSignal : intégration RxJS
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { toSignal } from '@angular/core/rxjs-interop';
@Component({
selector: 'app-users',
standalone: true,
template: `
@if (users()) {
@for (user of users(); track user.id) {
<p>{{ user.name }}</p>
}
} @else {
<p>Chargement...</p>
}
`
})
export class UsersComponent {
private http = inject(HttpClient);
// Observable RxJS converti en Signal directement dans le template
users = toSignal(this.http.get<User[]>('/api/users'));
}
4. computed vs effect : la distinction clé
C'est ici que beaucoup de débutants se trompent. Voici comment les différencier :
| Caractéristique | computed | effect |
|---|---|---|
| Objectif | Dériver une nouvelle donnée. | Produire un effet de bord. |
| Retour | Retourne un nouveau Signal. | Ne retourne rien. |
| Exemple | Calculer un prix TTC à partir d'un HT. | Sauvegarder dans le localStorage. |
| Règle d'or | Doit être une fonction pure. | Éviter d'y modifier d'autres Signals. |
⚠️ Attention : N'utilisez paseffectpour transformer des données. Si vous voulez dériver X en Y, utilisez systématiquementcomputed.
5. L'avenir : Vers le "Zoneless" et le Resource API
L'ambition de l'équipe Angular est claire : rendre le framework plus léger et plus performant, avec les Signals comme moteur central.
Composants "Zoneless"
Depuis Angular 18, il est possible d'opter pour un mode expérimental sans Zone.js. Les Signals deviennent alors le seul mécanisme de détection de changement — Angular sait exactement quel composant mettre à jour, sans parcourir tout l'arbre.
Le Resource API : une couche réactive par-dessus fetch
Apparu en Developer Preview avec Angular 19, le Resource API propose une couche réactive pilotée par Signals pour gérer des requêtes asynchrones. Il ne remplace pas HttpClient, mais offre une alternative plus simple pour les cas d'usage courants — sans switchMap, sans subscribe.
⚠️ Note : Le Resource API est encore en évolution. Avant de l'adopter en production, consultez la documentation officielle pour vérifier son statut de stabilité.
import { Component, signal, resource } from '@angular/core';
@Component({
selector: 'app-user-profile',
standalone: true,
template: `
@if (userResource.isLoading()) {
<p>Chargement...</p>
} @else if (userResource.error()) {
<p>Erreur de chargement.</p>
} @else {
<p>{{ userResource.value()?.name }}</p>
}
`
})
export class UserProfileComponent {
userId = signal(1);
userResource = resource({
request: () => ({ id: this.userId() }),
loader: ({ request }) =>
fetch(`/api/users/${request.id}`).then(r => r.json())
});
}
Le Resource API réactive automatiquement la requête quand userId change — sans switchMap, sans subscribe.
En résumé : RxJS reste l'outil de prédilection pour l'asynchronisme complexe et l'événementiel. Mais pour la gestion d'état, les calculs dérivés et les requêtes HTTP simples, les Signals sont désormais la voie recommandée. La migration n'est pas une révolution à opérer du jour au lendemain — toSignal() est précisément là pour vous permettre d'avancer progressivement. Il est temps de commencer !