Quand j’ai découvert RxJS il y a quelques années avec Angular, je voyais juste des .subscribe() partout sur des trucs qu’on appelait des observables.
Mais ce que je ne voyais pas encore, c’étaient les fuites mémoire, les comportements bizarres, et les chaînes d’opérateurs bancales.
Avec le temps, j’ai compris que la programmation réactive, c’est bien plus qu’un outil pour gérer des appels API. C’est un changement de paradigme, qui implique aussi de bonnes pratiques et un modèle mental différent.
Mais avant tout, c’est quoi un Observable ?
Un observable est un flux de données asynchrone que l’on peut écouter dans le temps.
Il est au coeur de la programmation réactive avec RxJS.
L’idée de base : une source qui émet des valeurs
Imaginez un tuyau dans lequel des données arrivent au fil du temps :
const obs$ = new Observable(observer => {
observer.next(1);
observer.next(2);
observer.next(3);
observer.complete();
});
Ici, on crée un flux qui émet 1, 2, 3, puis s’arrête.
Pour “écouter” ce flux, on utilise subscribe() :
obs$.subscribe(value => console.log(value));
// affiche : 1, 2, 3
Différence avec une Promise
| Feature | Promise | Observable |
|---|---|---|
| Valeurs | Une seule | Plusieurs dans le temps |
| Annulable | Non | Oui |
| Lazy (sur demande) | Oui | Oui |
| Opérateurs | .then(), .catch() | .pipe(), map(), filter()… |
| Exécution | Dès création | Au moment du subscribe() |
Exemple concret : une recherche utilisateur
searchInput$.pipe(
debounceTime(300),
switchMap(query => this.api.search(query))
).subscribe(results => {
this.results = results;
});
searchInput$est un observable d’événements clavier : on récupère les modifications du champ “Rechercher un utilisateur”debounceTime(300): attend une pause de 300ms : on veut éviter de lancer une recherche à chaque fois qu’un utilisateur appuie sur une touche. Ledebounceici attend 300ms après la dernière valeur émise pour continuer le fluxswitchMap(...): on vient switcher d’observable dans le flux :this.api.search(query)va instancier un nouvel observable (une requête http) qu’on a très envie d’attendre pour récupérer son résultat. On switch donc de fluxsubscribe(...): réceptionne les résultats et met à jour l’UI (en vrai, si vous faîtes du Angular, je préfère le pipe| async)
Un observable peut :
- émettre 0, 1 ou plusieurs valeurs
- être infini (par exemple :
fromEvent,interval) - être froid ou chaud (voir la section suivante)
- être combiné, transformé, filtré, temporisé, réessayé, etc.
RxJS fournit plus de 60 opérateurs pour composer des flux de manière expressive et lisible.
Observable froid vs chaud : comprendre la différence
Observable froid
Un observable est dit “froid” quand la source est réévaluée à chaque abonnement. Cela signifie que chaque souscripteur a son propre cycle de vie.
const cold$ = new Observable(observer => {
console.log('Nouvel abonné');
observer.next(Math.random());
});
cold$.subscribe(val => console.log('Abonné 1 :', val));
cold$.subscribe(val => console.log('Abonné 2 :', val));
Nouvel abonné
Abonné 1 : 0.42
Nouvel abonné
Abonné 2 : 0.88
http.get(), of(), from()… Observable chaud
Un observable est dit “chaud” quand il partage sa source de données entre les abonnés.
const subject = new Subject();
subject.subscribe(val => console.log('Abonné 1 :', val));
subject.next(1);
subject.next(2);
subject.subscribe(val => console.log('Abonné 2 :', val));
subject.next(3);
Abonné 1 : 1
Abonné 1 : 2
Abonné 1 : 3
Abonné 2 : 3
Subject, fromEvent, WebSocket, etc. Froid + partage = chaud (avec shareReplay)
Il est possible de rendre un observable froid “chaud” en le partageant avec share() ou shareReplay() :
const api$ = this.http.get('/data').pipe(shareReplay(1));
Cela évite de déclencher plusieurs requêtes HTTP si plusieurs abonnés s’inscrivent.
Les bonnes pratiques à adopter
Un grand pouvoir implique de grandes responsabilités, comme dirait l’oncle.
1. Toujours gérer la désinscription
Un subscribe() sans unsubscribe() = fuite mémoire assurée.
Par défaut les souscriptions aux observables n’entraînent pas de désinscription automatique (comme les évènements JS en fait).
Voici ce qu’il faut faire :
-
Utiliser
takeUntil()outake(1): l’opérateur vient compléter l’observable automatiquementtakeUntil(notifer$)
Il fonctionne en écoutant un autre observable
notifier$qui, lorsqu’il émet, provoque la complétion (et donc la désinscription) de la source observable.Il ne déclenche pas l’unsubscribe tant que ce notifier n’émet pas, mais une fois que c’est fait, la désinscription est automatique.
take(1)complète automatiquement l’observable après la première émission, ce qui entraîne un unsubscribe automatique pour cet abonnement.
-
Désinscrire
unsubscribe()dansngOnDestroyde votre composant -
Utiliser
asyncpipe dans les templates Angular : Leasyncpipe s’occupe de tout : il souscrit ET désinscrit automatiquement.
<div *ngIf="data$ | async as data">
{{ data.title }}
</div>
2. Éviter les souscriptions imbriquées
Ne faites surtout pas ceci :
this.a$.subscribe(a => {
this.b$.subscribe(b => {
// ...
});
});
Utilisez switchMap, mergeMap, concatMap, selon le comportement voulu :
this.result$ = this.a$.pipe(
switchMap(a => this.b$)
);
Pourquoi ?
- Parce qu’il faut trois boîtes d’Aspirine pour lire une pyramide de la mort (tu sais, quand tu as un paquet de
subscribe()imbriqués…) - La gestion des erreurs devient compliquée : chaque
subscribe()gère ses propres erreurs. L’imbrication = acrobaties assurées pour propager ou centraliser les erreurs correctement - Fuites mémoires garanties
- Ordre d’exécution non maîtrisé
3. Choisir le bon opérateur
Il en existe un bon gros paquet, alors n’hésitons pas à prendre le plus pertinent ! Quelques-uns que j’utilise le plus :
-
switchMap(): annule l’observable précédent à chaque nouvelle émissionUtile pour des recherches utilisateur en temps réel.
-
debounceTime(500): attend un délai de calme avant d’émettreUtile pour réduire le bruit sur les champs de recherche.
-
map(val => val * 2): transforme les valeursManipulation de données simples.
-
throttleTime(2000): ignore les émissions trop rapprochéesAnti-spam sur bouton de soumission.
-
shareReplay(1): partage et met en cache les dernières valeursPour éviter de rappeler une API 5 fois pour 5 abonnés.
Pour aller plus loin : learnrxjs.io
Les erreurs les plus courantes avec RxJS
1. Oublier de se désinscrire
C’est le piège le plus fréquent. Un subscribe() oublié dans un composant Angular = une souscription qui survit à la destruction du composant, et qui continue d’écouter des événements en arrière-plan.
// A NE PAS FAIRE
ngOnInit() {
this.dataService.getData().subscribe(data => {
this.data = data; // cette callback tourne encore après ngOnDestroy
});
}
2. Souscrire dans un subscribe
Déjà évoqué plus haut, mais c’est tellement courant que ça mérite d’être répété. L’imbrication de subscribe() est la source principale de code illisible et de fuites mémoire en Angular.
3. Confondre switchMap et mergeMap
Utiliser mergeMap là où switchMap est attendu (typiquement une recherche) signifie que les requêtes précédentes ne sont pas annulées, et les résultats arrivent dans le désordre.
4. Ne pas gérer les erreurs dans le pipe
Un catchError manquant dans un pipe peut tuer silencieusement le flux entier. Toujours prévoir un fallback :
this.data$ = this.http.get('/api/data').pipe(
catchError(err => {
console.error('Erreur API:', err);
return of([]); // fallback: tableau vide
})
);
5. Abuser de shareReplay sans limiter
shareReplay() sans paramètre garde tout en mémoire. Préférez shareReplay(1) pour ne garder que la dernière valeur, et shareReplay({ bufferSize: 1, refCount: true }) pour libérer la souscription source quand plus personne n’écoute.
Guide de choix des opérateurs de “flattening”
| Opérateur | Comportement | Cas d’usage typique |
|---|---|---|
| switchMap | Annule l’observable précédent à chaque nouvelle émission | Recherche en temps réel, autocomplétion |
| mergeMap | Exécute tous les observables en parallèle | Envoi de logs, actions indépendantes |
| concatMap | Exécute les observables dans l’ordre, un par un | File d’attente de requêtes, opérations séquentielles |
| exhaustMap | Ignore les nouvelles émissions tant que la précédente n’est pas terminée | Soumission de formulaire (anti double-clic) |
En cas de doute, commencez par switchMap. C’est le plus sûr dans la majorité des cas (recherche, navigation, filtres). N’utilisez mergeMap que si vous avez vraiment besoin que toutes les requêtes s’exécutent en parallèle.
Tester ses observables : le marble testing
RxJS fournit un mécanisme de test appelé marble testing qui permet de décrire des flux de manière visuelle :
import { TestScheduler } from 'rxjs/testing';
const scheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
scheduler.run(({ cold, expectObservable }) => {
const source$ = cold(' -a-b-c|');
const expected = ' -A-B-C|';
const result$ = source$.pipe(map(v => v.toUpperCase()));
expectObservable(result$).toBe(expected);
});
Chaque caractère représente une unité de temps (frame) : - est un tick vide, une lettre est une émission, | est la complétion, # est une erreur. C’est un outil puissant pour tester des flux complexes (debounce, switchMap, retry…) de manière déterministe.
Attention à penser à long terme
RxJS est un outil extrêmement puissant, mais sans bonnes pratiques, il peut rapidement devenir difficile à gérer.
Avant de plonger dans le code, prenez le temps de réfléchir à :
- La “température” de vos observables : distinguez bien les observables “froids” (qui produisent des données à chaque abonnement) des “chauds” (qui émettent indépendamment des abonnés).
- Qui s’abonne, quand et pourquoi : comprenez bien le cycle de vie de vos abonnements pour éviter les fuites mémoire.
- Comment gérer l’annulation : prévoyez toujours comment et quand vos abonnements doivent être nettoyés (désabonnés).
- La robustesse de vos flux : imaginez vos chaînes d’opérateurs capables de gérer les erreurs, les interruptions, et même d’évoluer sans casser l’application (par exemple avec des rollbacks logiques).
Enfin, testez vos flux RxJS indépendamment des composants pour garantir leur fiabilité et faciliter la maintenance.
En résumé
RxJS n’est pas juste un outil pour “faire des appels API” ou “écouter des événements”.
C’est une manière structurée et déclarative de penser la gestion des flux de données asynchrones.
Et comme tout outil puissant mal utilisé… ça peut devenir un cauchemar.
Et vous, quels sont vos opérateurs RxJS préférés ?

