Erreurs courantes et solutions
Cette page explique en détail 15 anti-patterns fréquemment rencontrés lors de l'utilisation de RxJS avec TypeScript, et leurs solutions respectives.
Table des matières
- Exposition externe des Subjects
- Souscriptions imbriquées (callback hell)
- Oubli d'unsubscribe (fuites mémoire)
- Mauvaise utilisation de shareReplay
- Effets de bord dans map
- Ignorer les différences Cold/Hot Observable
- Mélange inapproprié de Promise et Observable
- Ignorer le backpressure
- Suppression d'erreurs
- Fuites de souscriptions d'événements DOM
- Manque de sécurité de type (utilisation excessive de any)
- Sélection d'opérateur inappropriée
- Complexification excessive
- Modification d'état dans subscribe
- Absence de tests
1. Exposition externe des Subjects
Problème
Exposer directement un Subject permet aux externes d'appeler next(), rendant la gestion d'état imprévisible.
❌ Mauvais exemple
import { Subject } from 'rxjs';
// Export direct du Subject
export const cartChanged$ = new Subject<void>();
// N'importe qui peut appeler next() depuis un autre fichier
cartChanged$.next(); // Peut être appelé à des moments inattendus✅ Bon exemple
import { BehaviorSubject, Observable } from 'rxjs';
class CartStore {
private readonly _items$ = new BehaviorSubject<string[]>([]);
// Exposer comme Observable en lecture seule
readonly items$: Observable<string[]> = this._items$.asObservable();
// Contrôler les changements d'état via des méthodes dédiées
add(item: string): void {
this._items$.next([...this._items$.value, item]);
}
remove(item: string): void {
this._items$.next(
this._items$.value.filter(i => i !== item)
);
}
}
export const cartStore = new CartStore();Explication
- Convertir en
Observableen lecture seule avecasObservable() - Permettre les changements d'état uniquement via des méthodes dédiées
- Améliorer la traçabilité des changements et faciliter le débogage
2. Souscriptions imbriquées (callback hell)
Problème
Appeler subscribe à l'intérieur d'un autre subscribe conduit au callback hell, compliquant la gestion d'erreurs et l'annulation.
❌ Mauvais exemple
import { of } from 'rxjs';
// Simulation d'appels API
function apiA() {
return of({ id: 1 });
}
function apiB(id: number) {
return of({ id, token: 'abc123' });
}
function apiC(token: string) {
return of({ success: true });
}
// Souscriptions imbriquées
apiA().subscribe(a => {
apiB(a.id).subscribe(b => {
apiC(b.token).subscribe(result => {
console.log('done', result);
});
});
});✅ Bon exemple
import { of } from 'rxjs';
import { switchMap } from 'rxjs';
function apiA() {
return of({ id: 1 });
}
function apiB(id: number) {
return of({ id, token: 'abc123' });
}
function apiC(token: string) {
return of({ success: true });
};
// Aplatir avec des opérateurs d'ordre supérieur
apiA().pipe(
switchMap(a => apiB(a.id)),
switchMap(b => apiC(b.token))
).subscribe(result => {
console.log('done', result);
});Explication
- Utiliser les opérateurs d'ordre supérieur comme
switchMap,mergeMap,concatMap - Gestion d'erreurs possible en un seul endroit
- Désinscription en une seule fois
- Lisibilité du code améliorée
3. Oubli d'unsubscribe (fuites mémoire)
Problème
Ne pas se désabonner des flux infinis (comme les écouteurs d'événements) provoque des fuites mémoire.
❌ Mauvais exemple
import { fromEvent } from 'rxjs';
// Lors de l'initialisation du composant
function setupResizeHandler() {
fromEvent(window, 'resize').subscribe(() => {
console.log('resized');
});
// Pas de désinscription !
}
// L'écouteur d'événements persiste même après la destruction du composant✅ Bon exemple
import { fromEvent, Subject } from 'rxjs';
import { takeUntil, finalize } from 'rxjs';
class MyComponent {
private readonly destroy$ = new Subject<void>();
ngOnInit(): void {
fromEvent(window, 'resize').pipe(
takeUntil(this.destroy$),
finalize(() => console.log('cleanup'))
).subscribe(() => {
console.log('resized');
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}✅ Autre bon exemple (utilisant Subscription)
import { fromEvent, Subscription } from 'rxjs';
class MyComponent {
private subscription = new Subscription();
ngOnInit(): void {
this.subscription.add(
fromEvent(window, 'resize').subscribe(() => {
console.log('resized');
})
);
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
}Explication
- Le pattern
takeUntilest recommandé (déclaratif et clair) - La gestion manuelle avec
Subscriptionest également efficace - Toujours exécuter la désinscription lors de la destruction du composant
4. Mauvaise utilisation de shareReplay
Problème
Utiliser shareReplay sans comprendre son fonctionnement peut entraîner la relecture de données périmées ou des fuites mémoire.
❌ Mauvais exemple
import { interval } from 'rxjs';
import { shareReplay, take } from 'rxjs';
// Taille de buffer illimitée
const shared$ = interval(1000).pipe(
shareReplay() // Buffer illimité par défaut
);
// Les valeurs restent en mémoire même sans abonnés✅ Bon exemple
import { interval } from 'rxjs';
import { shareReplay, take } from 'rxjs';
// Spécifier explicitement la taille du buffer et le comptage de références
const shared$ = interval(1000).pipe(
take(10),
shareReplay({
bufferSize: 1,
refCount: true // Libérer les ressources quand il n'y a plus d'abonnés
})
);Explication
- Spécifier explicitement
bufferSize(généralement 1) refCount: truepour libérer automatiquement quand il n'y a plus d'abonnés- Pour les flux qui se terminent comme les requêtes HTTP,
shareReplay({ bufferSize: 1, refCount: true })est sûr
5. Effets de bord dans map
Problème
Modifier l'état dans l'opérateur map provoque un comportement imprévisible.
❌ Mauvais exemple
import { of } from 'rxjs';
import { map } from 'rxjs';
let counter = 0;
const source$ = of(1, 2, 3).pipe(
map(value => {
counter++; // Effet de bord !
return value * 2;
})
);
source$.subscribe(console.log);
source$.subscribe(console.log); // counter augmente de manière inattendue✅ Bon exemple
import { of } from 'rxjs';
import { map, tap, scan } from 'rxjs';
// Transformation pure uniquement
const source$ = of(1, 2, 3).pipe(
map(value => value * 2)
);
// Séparer les effets de bord avec tap
const withLogging$ = source$.pipe(
tap(value => console.log('Processing:', value))
);
// Utiliser scan pour l'accumulation d'état
const withCounter$ = of(1, 2, 3).pipe(
scan((acc, value) => ({ count: acc.count + 1, value }), { count: 0, value: 0 })
);Explication
- Utiliser
mapcomme fonction pure - Séparer les effets de bord (logs, appels API, etc.) avec
tap - Utiliser
scanoureducepour l'accumulation d'état
6. Ignorer les différences Cold/Hot Observable
Problème
Utiliser des Observables sans comprendre s'ils sont Cold ou Hot provoque des exécutions en double ou des comportements inattendus.
❌ Mauvais exemple
import { ajax } from 'rxjs/ajax';
// Cold Observable - une requête HTTP est exécutée pour chaque souscription
const data$ = ajax.getJSON('https://api.example.com/data');
data$.subscribe(console.log); // Requête 1
data$.subscribe(console.log); // Requête 2 (duplication inutile)✅ Bon exemple
import { ajax } from 'rxjs/ajax';
import { shareReplay } from 'rxjs';
// Convertir en Hot Observable pour partager
const data$ = ajax.getJSON('https://api.example.com/data').pipe(
shareReplay({ bufferSize: 1, refCount: true })
);
data$.subscribe(console.log); // Requête 1
data$.subscribe(console.log); // Utilise le résultat en cacheExplication
- Cold Observable : exécuté pour chaque souscription (
of,from,fromEvent,ajax, etc.) - Hot Observable : exécuté indépendamment des souscriptions (
Subject, Observable multicast, etc.) - Convertir Cold en Hot avec
share/shareReplay
7. Mélange inapproprié de Promise et Observable
Problème
Mélanger Promise et Observable sans conversion appropriée rend la gestion d'erreurs et l'annulation incomplètes.
❌ Mauvais exemple
import { from } from 'rxjs';
async function fetchData(): Promise<string> {
return 'data';
}
// Utiliser Promise directement
from(fetchData()).subscribe(data => {
fetchData().then(moreData => { // Promise imbriquée
console.log(data, moreData);
});
});✅ Bon exemple
import { from } from 'rxjs';
import { switchMap } from 'rxjs';
async function fetchData(): Promise<string> {
return 'data';
}
// Convertir Promise en Observable pour unifier
from(fetchData()).pipe(
switchMap(() => from(fetchData()))
).subscribe(moreData => {
console.log(moreData);
});Explication
- Convertir Promise en Observable avec
from - Traiter de manière unifiée dans le pipeline Observable
- Gestion d'erreurs et annulation facilitées
8. Ignorer le backpressure
Problème
Traiter des événements à haute fréquence sans contrôle dégrade les performances.
❌ Mauvais exemple
import { fromEvent } from 'rxjs';
// Traiter les événements d'entrée tels quels
fromEvent(document.getElementById('search'), 'input').subscribe(event => {
// Appel API à chaque saisie (surcharge)
searchAPI((event.target as HTMLInputElement).value);
});
function searchAPI(query: string): void {
console.log('Searching for:', query);
}✅ Bon exemple
import { fromEvent } from 'rxjs';
import { debounceTime, distinctUntilChanged, map, switchMap } from 'rxjs';
// Appliquer debounce et annulation
fromEvent(document.getElementById('search'), 'input').pipe(
map(event => (event.target as HTMLInputElement).value),
debounceTime(300), // Attendre 300ms
distinctUntilChanged(), // Uniquement quand la valeur change
switchMap(query => searchAPI(query)) // Annuler les anciennes requêtes
).subscribe(results => {
console.log('Results:', results);
});Explication
- Attendre un certain temps avec
debounceTime - Limiter la fréquence maximale avec
throttleTime - Exclure les doublons avec
distinctUntilChanged - Annuler les anciennes requêtes avec
switchMap
9. Suppression d'erreurs
Problème
Ne pas gérer correctement les erreurs rend le débogage difficile et dégrade l'expérience utilisateur.
❌ Mauvais exemple
import { ajax } from 'rxjs/ajax';
import { catchError } from 'rxjs';
import { of } from 'rxjs';
// Ignorer les erreurs
ajax.getJSON('https://api.example.com/data').pipe(
catchError(() => of(null)) // L'information d'erreur est perdue
).subscribe(data => {
console.log(data); // Si null, cause inconnue
});✅ Bon exemple
import { ajax } from 'rxjs/ajax';
import { catchError } from 'rxjs';
import { of } from 'rxjs';
interface ApiResponse {
data: unknown;
error?: string;
}
ajax.getJSON<ApiResponse>('https://api.example.com/data').pipe(
catchError(error => {
console.error('API Error:', error);
// Notifier l'utilisateur
showErrorToast('Échec de la récupération des données');
// Retourner une valeur alternative incluant l'information d'erreur
return of({ data: null, error: error.message } as ApiResponse);
})
).subscribe((response) => {
if (response.error) {
console.log('Fallback mode due to:', response.error);
}
});
function showErrorToast(message: string): void {
console.log('Toast:', message);
}Explication
- Enregistrer les erreurs dans les logs
- Fournir un retour à l'utilisateur
- Retourner une valeur alternative incluant l'information d'erreur
- Considérer une stratégie de retry (
retry,retryWhen)
10. Fuites de souscriptions d'événements DOM
Problème
Ne pas libérer correctement les écouteurs d'événements DOM provoque des fuites mémoire.
❌ Mauvais exemple
import { fromEvent } from 'rxjs';
class Widget {
private button: HTMLButtonElement;
constructor() {
this.button = document.createElement('button');
// Enregistrer un écouteur d'événements
fromEvent(this.button, 'click').subscribe(() => {
console.log('clicked');
});
// Pas de désinscription
}
destroy(): void {
this.button.remove();
// L'écouteur reste en mémoire
}
}✅ Bon exemple
import { fromEvent, Subject } from 'rxjs';
import { takeUntil } from 'rxjs';
class Widget {
private button: HTMLButtonElement;
private readonly destroy$ = new Subject<void>();
constructor() {
this.button = document.createElement('button');
fromEvent(this.button, 'click').pipe(
takeUntil(this.destroy$)
).subscribe(() => {
console.log('clicked');
});
}
destroy(): void {
this.destroy$.next();
this.destroy$.complete();
this.button.remove();
}
}Explication
- Désinscription garantie avec le pattern
takeUntil - Déclencher
destroy$lors de la destruction du composant - Libérer les écouteurs avant de supprimer l'élément DOM
11. Manque de sécurité de type (utilisation excessive de any)
Problème
L'utilisation excessive de any désactive la vérification de type de TypeScript, augmentant les risques d'erreurs au runtime.
❌ Mauvais exemple
import { Observable } from 'rxjs';
import { map } from 'rxjs';
function fetchUser(): Observable<any> {
return new Observable(subscriber => {
subscriber.next({ name: 'John', age: 30 });
});
}
// Pas de vérification de type
fetchUser().pipe(
map(user => user.naem) // Faute de frappe ! Non détectée jusqu'au runtime
).subscribe(console.log);✅ Bon exemple
import { Observable } from 'rxjs';
import { map } from 'rxjs';
interface User {
name: string;
age: number;
}
function fetchUser(): Observable<User> {
return new Observable<User>(subscriber => {
subscriber.next({ name: 'John', age: 30 });
});
}
// Vérification de type active
fetchUser().pipe(
map(user => user.name) // Erreur détectée à la compilation
).subscribe(console.log);Explication
- Définir des interfaces ou des alias de types
- Spécifier explicitement le paramètre de type de
Observable<T> - Utiliser pleinement l'inférence de type de TypeScript
12. Sélection d'opérateur inappropriée
Problème
Utiliser un opérateur inadapté à l'objectif peut être inefficace ou provoquer un comportement inattendu.
❌ Mauvais exemple
import { fromEvent } from 'rxjs';
import { mergeMap } from 'rxjs';
import { ajax } from 'rxjs/ajax';
// Recherche à chaque clic (les anciennes requêtes ne sont pas annulées)
fromEvent(document.getElementById('search-btn'), 'click').pipe(
mergeMap(() => ajax.getJSON('https://api.example.com/search'))
).subscribe(console.log);✅ Bon exemple
import { fromEvent } from 'rxjs';
import { switchMap } from 'rxjs';
import { ajax } from 'rxjs/ajax';
// Traiter uniquement la dernière requête (annulation automatique des anciennes)
fromEvent(document.getElementById('search-btn'), 'click').pipe(
switchMap(() => ajax.getJSON('https://api.example.com/search'))
).subscribe(console.log);Utilisation des principaux opérateurs d'ordre supérieur
| Opérateur | Usage |
|---|---|
switchMap | Traiter uniquement le dernier flux (recherche, auto-complétion) |
mergeMap | Traitement parallèle (ordre non important) |
concatMap | Traitement séquentiel (ordre important) |
exhaustMap | Ignorer les nouvelles entrées pendant l'exécution (anti-spam de bouton) |
Explication
- Comprendre le comportement de chaque opérateur
- Sélection appropriée selon le cas d'usage
- Voir Opérateurs de transformation pour plus de détails
13. Complexification excessive
Problème
Complexifier excessivement avec RxJS des traitements qui pourraient être simples.
❌ Mauvais exemple
import { Observable, of } from 'rxjs';
import { map, mergeMap, toArray } from 'rxjs';
// Complexifier une simple transformation de tableau avec RxJS
function doubleNumbers(numbers: number[]): Observable<number[]> {
return of(numbers).pipe(
mergeMap(arr => of(...arr)),
map(n => n * 2),
toArray()
);
}✅ Bon exemple
import { fromEvent } from 'rxjs';
import { map } from 'rxjs';
// Le traitement de tableaux est suffisant avec du JavaScript normal
function doubleNumbers(numbers: number[]): number[] {
return numbers.map(n => n * 2);
}
// Utiliser RxJS pour le traitement asynchrone et événementiel
const button = document.getElementById('calc-btn') as HTMLButtonElement;
const numbers = [1, 2, 3, 4, 5];
fromEvent(button, 'click').pipe(
map(() => doubleNumbers(numbers))
).subscribe(result => console.log(result));Explication
- Utiliser RxJS pour le traitement asynchrone et les flux d'événements
- Le traitement synchrone de tableaux est suffisant avec du JavaScript normal
- Considérer l'équilibre entre complexité et bénéfices
14. Modification d'état dans subscribe
Problème
Modifier directement l'état dans subscribe rend les tests difficiles et devient une source de bugs.
❌ Mauvais exemple
import { interval } from 'rxjs';
class Counter {
count = 0;
start(): void {
interval(1000).subscribe(() => {
this.count++; // Modification d'état dans subscribe
this.updateUI();
});
}
updateUI(): void {
console.log('Count:', this.count);
}
}✅ Bon exemple
import { interval, BehaviorSubject } from 'rxjs';
import { scan, tap } from 'rxjs';
class Counter {
private readonly count$ = new BehaviorSubject<number>(0);
start(): void {
interval(1000).pipe(
scan(acc => acc + 1, 0),
tap(count => this.count$.next(count))
).subscribe();
// L'UI s'abonne à count$
this.count$.subscribe(count => this.updateUI(count));
}
updateUI(count: number): void {
console.log('Count:', count);
}
}Explication
- Gérer l'état avec
BehaviorSubjectouscan - Utiliser
subscribecomme déclencheur - Conception testable et réactive
15. Absence de tests
Problème
Déployer du code RxJS en production sans tests augmente les risques de régression.
❌ Mauvais exemple
import { interval } from 'rxjs';
import { map, filter } from 'rxjs';
// Déploiement sans tests
export function getEvenNumbers() {
return interval(1000).pipe(
filter(n => n % 2 === 0),
map(n => n * 2)
);
}✅ Bon exemple
import { TestScheduler } from 'rxjs/testing';
import { getEvenNumbers } from './numbers';
describe('getEvenNumbers', () => {
let scheduler: TestScheduler;
beforeEach(() => {
scheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
});
it('should emit only even numbers doubled', () => {
scheduler.run(({ expectObservable }) => {
const expected = '1s 0 1s 4 1s 8';
expectObservable(getEvenNumbers()).toBe(expected);
});
});
});Explication
- Effectuer des tests marble avec
TestScheduler - Possibilité de tester le traitement asynchrone de manière synchrone
- Voir Méthodes de test pour plus de détails
Résumé
Comprendre et éviter ces 15 anti-patterns vous permettra d'écrire du code RxJS plus robuste et maintenable.
Références
Cette collection d'anti-patterns a été créée en référence aux sources fiables suivantes.
Documentation officielle et dépôts
- Documentation officielle RxJS - Référence officielle des opérateurs et de l'API
- GitHub Issue #5931 - Discussion sur le problème de fuite mémoire de shareReplay
Anti-patterns et meilleures pratiques
- RxJS in Angular - Antipattern 1: Nested subscriptions - Thinktecture AG
- RxJS in Angular - Antipattern 2: Stateful Streams - Thinktecture AG
- RxJS Best Practices in Angular 16 (2025) - InfoQ (mai 2025)
- RxJS: Why memory leaks occur when using a Subject - Angular In Depth
- RxJS Antipatterns - Brian Love
Ressources supplémentaires
- Learn RxJS - Guide pratique des opérateurs et patterns
- RxJS Marbles - Compréhension visuelle des opérateurs
Utiliser pour la revue de code
Vérifiez si votre code correspond à un anti-pattern.
👉 Checklist d'évitement des anti-patterns - Révisez votre code avec 15 points de vérification
Chaque point de vérification permet d'accéder directement aux détails de l'anti-pattern correspondant sur cette page.
Prochaines étapes
- Gestion des erreurs - Apprendre des stratégies de gestion d'erreurs plus détaillées
- Méthodes de test - Maîtriser les méthodes efficaces de test du code RxJS
- Comprendre les opérateurs - Apprendre l'utilisation détaillée de chaque opérateur
Intégrez ces meilleures pratiques dans votre codage quotidien pour écrire du code RxJS de haute qualité !