Appearance
Observable Angular : comprendre RxJS, async pipe et Signals
Un Observable est un objet RxJS qui représente une source de valeurs dans le temps. Il peut émettre une valeur, plusieurs valeurs, une erreur, puis se terminer.
Dans Angular, les Observables sont partout :
HttpClientretourne des Observables pour les requêtes HTTP ;- le Router expose des Observables pour les paramètres et les événements de navigation ;
- les formulaires réactifs exposent
valueChangesetstatusChanges; - RxJS permet de composer des flux avec des opérateurs comme
map,filter,switchMap,catchErroroudebounceTime.
Depuis l'arrivée des Signals, la question n'est plus "Observable ou Signal ?" mais plutôt : quelle abstraction utiliser à quel endroit ?
L'idée simple
Un Observable est comme une chaîne d'informations à laquelle on peut s'abonner. Tant que personne ne s'abonne, beaucoup d'Observables ne font rien. Quand une valeur arrive, l'Observable la pousse vers ses abonnés.
ts
import { Observable } from 'rxjs';
const messages$ = new Observable<string>((subscriber) => {
subscriber.next('Bonjour');
subscriber.next('Bienvenue dans Angular');
subscriber.complete();
});
messages$.subscribe({
next: (message) => console.log(message),
error: (error) => console.error(error),
complete: () => console.log('Terminé'),
});Dans cet exemple :
messages$est l'Observable ;- la fonction passée à
subscribe()est l'abonnement ; nextreçoit les valeurs ;errorreçoit les erreurs ;completeindique que le flux est terminé.
Pourquoi le signe $ ?
Le suffixe $ n'est pas obligatoire, mais il est très courant pour indiquer qu'une variable contient un Observable : users$, routeParams$, searchResults$.
Observable, Observer et Subscription
Trois mots reviennent souvent avec RxJS :
| Concept | Rôle |
|---|---|
Observable | La source de valeurs dans le temps |
Observer | L'objet qui sait réagir à next, error et complete |
Subscription | Le lien actif entre l'Observable et l'abonné |
Une souscription peut être arrêtée :
ts
import { interval } from 'rxjs';
const subscription = interval(1000).subscribe((value) => {
console.log(value);
});
setTimeout(() => {
subscription.unsubscribe();
}, 5000);Ici, interval(1000) émet une valeur toutes les secondes. Après cinq secondes, unsubscribe() coupe l'abonnement.
Synchrone ou asynchrone ?
Un Observable peut être synchrone ou asynchrone.
ts
import { of, timer } from 'rxjs';
of(1, 2, 3).subscribe((value) => console.log(value));
timer(1000).subscribe(() => {
console.log('Une seconde plus tard');
});of(1, 2, 3) émet immédiatement. timer(1000) émet plus tard.
C'est une différence importante avec une Promise : une Promise représente une valeur future unique, alors qu'un Observable peut représenter zéro, une ou plusieurs valeurs.
Observable ou Promise ?
Utilisez une Promise quand vous attendez une seule valeur et que vous n'avez pas besoin d'annuler ou de composer un flux.
Utilisez un Observable quand :
- il peut y avoir plusieurs valeurs dans le temps ;
- vous voulez annuler l'écoute ;
- vous voulez transformer, filtrer ou combiner des événements ;
- vous travaillez avec les API Angular qui retournent déjà des Observables.
ts
import { fromEvent } from 'rxjs';
const clicks$ = fromEvent(document, 'click');
clicks$.subscribe(() => {
console.log('Clic détecté');
});Un clic peut arriver plusieurs fois. Un Observable est donc plus naturel qu'une Promise pour ce cas.
Le cas le plus courant : HttpClient
Avec Angular, HttpClient retourne un Observable.
ts
import { HttpClient } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';
export interface User {
id: number;
name: string;
}
@Injectable({ providedIn: 'root' })
export class UsersService {
private readonly http = inject(HttpClient);
getUsers(): Observable<User[]> {
return this.http.get<User[]>('/api/users');
}
}Dans un composant, évitez de souscrire juste pour copier la donnée dans une propriété quand le template peut lire directement l'Observable.
ts
import { AsyncPipe } from '@angular/common';
import { Component, inject } from '@angular/core';
import { UsersService } from './users.service';
@Component({
selector: 'app-users',
imports: [AsyncPipe],
template: `
@for (user of users$ | async; track user.id) {
<p>{{ user.name }}</p>
}
`,
})
export class UsersComponent {
private readonly usersService = inject(UsersService);
readonly users$ = this.usersService.getUsers();
}Le pipe async s'abonne, renvoie la dernière valeur reçue, déclenche la détection de changement, puis se désabonne automatiquement quand le composant est détruit. Il se désabonne aussi de l'ancien Observable si la référence change.
Quand faut-il se désabonner ?
Tout dépend de la manière dont l'Observable est consommé.
| Situation | Que faire ? |
|---|---|
| Observable lu dans le template | Utiliser le pipe async |
Observable converti en Signal avec toSignal() | Angular nettoie automatiquement dans le contexte d'injection |
subscribe() manuel dans un composant | Utiliser takeUntilDestroyed() |
| Observable qui se termine seul, comme une requête HTTP | Le risque de fuite est plus faible, mais le pipe async ou toSignal() restent plus lisibles |
Flux long, comme WebSocket, interval, événements DOM | Prévoir un nettoyage explicite |
Exemple avec takeUntilDestroyed() :
ts
import { Component, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { fromEvent } from 'rxjs';
@Component({
selector: 'app-tracker',
template: `Position suivie`,
})
export class TrackerComponent {
constructor() {
fromEvent<MouseEvent>(document, 'mousemove')
.pipe(takeUntilDestroyed())
.subscribe((event) => {
console.log(event.clientX, event.clientY);
});
}
}takeUntilDestroyed() est utile quand vous devez vraiment appeler subscribe() vous-même.
Observable ou Signal ?
Un Signal représente une valeur actuelle, lisible de façon synchrone avec user(). Un Observable représente un flux de valeurs dans le temps.
En pratique :
- gardez les Observables pour les événements, les requêtes, les WebSockets, les formulaires, le Router et les compositions RxJS ;
- utilisez les Signals pour l'état local d'un composant ou d'un service ;
- convertissez avec
toSignal()quand un flux RxJS doit être lu facilement dans le template ou dans uncomputed().
ts
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { UsersService } from './users.service';
@Component({
selector: 'app-users-count',
template: `<p>{{ users().length }} utilisateurs</p>`,
})
export class UsersCountComponent {
private readonly usersService = inject(UsersService);
readonly users = toSignal(this.usersService.getUsers(), {
initialValue: [],
});
}toSignal() crée une souscription immédiatement. Il faut donc éviter de l'appeler plusieurs fois pour le même Observable. Créez le Signal une fois, puis réutilisez-le.
Convertir un Signal en Observable
L'autre sens existe aussi avec toObservable(). C'est utile quand une valeur en Signal doit entrer dans une chaîne RxJS.
ts
import { Component, inject, signal } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs';
interface SearchResult {
id: number;
title: string;
}
@Component({
selector: 'app-search',
template: `
<input
[value]="query()"
(input)="query.set($any($event.target).value)"
/>
`,
})
export class SearchComponent {
private readonly http = inject(HttpClient);
readonly query = signal('');
readonly results$ = toObservable(this.query).pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap((query) =>
this.http.get<SearchResult[]>('/api/search', {
params: { q: query },
}),
),
);
}Ici, le Signal garde la valeur du champ. RxJS gère le délai, évite les requêtes identiques, puis annule la requête précédente avec switchMap quand une nouvelle recherche arrive.
Subject et BehaviorSubject
Un Subject est à la fois un Observable et un Observer. On peut lui envoyer une valeur avec next(), puis exposer le flux aux consommateurs.
ts
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class CounterStore {
private readonly countSubject = new BehaviorSubject(0);
readonly count$ = this.countSubject.asObservable();
increment(): void {
this.countSubject.next(this.countSubject.value + 1);
}
}BehaviorSubject garde une valeur courante. C'est pratique pour un petit état partagé, mais ce n'est pas toujours le meilleur choix pour l'état local d'un composant : un signal() est souvent plus simple.
Les erreurs courantes
1. S'abonner dans un composant pour remplir une propriété
ts
// A eviter quand le template peut consommer l'Observable directement.
this.usersService.getUsers().subscribe((users) => {
this.users = users;
});Préférez users$ | async ou toSignal().
2. Oublier que subscribe() déclenche le travail
Certains Observables sont froids : chaque abonnement relance leur logique. Avec HttpClient, cela peut déclencher une nouvelle requête. Si vous avez besoin de partager le résultat, regardez du côté de shareReplay ou stockez la donnée dans un service.
3. Transformer un Observable en Signal trop souvent
ts
// A eviter : cree une nouvelle souscription a chaque appel.
get users() {
return toSignal(this.usersService.getUsers(), { initialValue: [] });
}Créez le Signal une seule fois :
ts
readonly users = toSignal(this.usersService.getUsers(), {
initialValue: [],
});4. Utiliser RxJS pour tout
RxJS est puissant, mais ce n'est pas une obligation pour chaque état. Pour une valeur locale comme isOpen, selectedTab ou searchText, un Signal est souvent plus direct.
Tableau de décision
| Besoin | Choix naturel |
|---|---|
| Une valeur locale modifiable dans un composant | signal() |
| Une valeur calculée depuis d'autres valeurs | computed() |
| Une requête HTTP Angular | Observable retourne par HttpClient, puis async ou toSignal() |
| Des événements répétés | Observable |
| Une recherche avec annulation | toObservable(signal).pipe(switchMap(...)) |
| Un petit état partagé historique | BehaviorSubject ou Signal dans un service |
| Une seule valeur future hors Angular | Promise |
A retenir
Un Observable sert à représenter un flux de valeurs dans le temps. Dans Angular, il reste essentiel pour HttpClient, le Router, les formulaires, les événements et les compositions RxJS.
Les Signals ne remplacent pas RxJS partout. Ils complètent le modèle : Signals pour l'état courant, Observables pour les flux.
Le bon réflexe est donc :
- lire les Observables dans le template avec le pipe
async; - utiliser
toSignal()quand vous voulez une valeur synchrone dans le composant ; - utiliser
takeUntilDestroyed()quand vous avez besoin d'unsubscribe()manuel ; - garder RxJS pour les cas où les opérateurs comme
switchMap,debounceTimeoucatchErrorapportent une vraie valeur.