Qu'est-ce que la programmation réactive avec RxJS ? RxJS (Reactive Extensions for JavaScript) est une bibliothèque de programmation réactive qui utilise Observables pour gérer des séquences d'événements asynchrones. Dans Angular, il est intégré nativement : chaque appel HttpClient, chaque FormControl.valueChanges, chaque événement Router est un observable. La vraie puissance de RxJS réside dans ses opérateurs : des fonctions pures qui transforment, filtrent, combinent et gèrent les flux de données. Bien les connaître fait la différence entre une application réactive bien conçue et un enchevêtrement de rappels imbriqués. 1. map — Transformation des données Analogie : Comme Array.map() , mais sur un flux continu dans le temps. Cas d'utilisation : Normaliser la réponse d'une API // Scénario : L'API renvoie { data: User[], total: number } // mais le composant attend uniquement User[] this.http.get>('/api/users') .pipe( map(response => response.data), map(users => users.map(u => ({ ...u, fullName: `${u.firstName} ${u.lastName}`, avatarUrl : u.avatar || '/assets/default-avatar.png' }))) ) .subscribe(users => this.users = users); Règle générale : utilisez map chaque fois que vous souhaitez transformer la forme des données sans modifier le taux de sortie. 2. filter — Filtrer les sorties Ne transmettre que les valeurs qui satisfont à un prédicat booléen. Cas d'utilisation : traiter uniquement les événements clavier pertinents fromEvent(document, 'keydown') .pipe( filter(event => event.key === 'Enter' || event.key === 'Escape'), map(event => event.key) ) .subscribe(key => { if (key === 'Enter') this.confirmAction(); if (key === 'Escape') this.closeModal(); Règle générale : utilisez le filtre le plus tôt possible dans la chaîne pour réduire le travail des opérateurs suivants. 3. tap – Effets secondaires sans modifier, tap effectue une action sur la valeur actuelle mais laisse la valeur passer inchangée. Idéal pour la journalisation et la mise à jour de l'état de l'interface utilisateur. Cas d'utilisation : journalisation + chargement de spinner this.http.get('/api/products') .
pipe( tap(() => this.loading = true), map(products => products.filter(p => p.inStock)), tap(products => console.log(`Produits disponibles : ${products.length}`)), tap(() => this.loading = false) ) .subscribe(products => this.products = products); Règle de base : n'utilisez pas Tap pour la logique métier, uniquement pour les effets secondaires (journal, analyse, spinner). 4. debounceTime + distinctUntilChanged — Optimiser la recherche Ces deux opérateurs travaillent souvent en paires pour éviter d'inonder le backend de requêtes inutiles au fur et à mesure que l'utilisateur tape. debounceTime(ms) : attend que l'utilisateur arrête de taper pendant N ms avant d'émettre distinctUntilChanged() : n'affiche pas si la valeur est identique à la précédente Cas d'utilisation : champ de recherche avec appel API this.searchControl.valueChanges.pipe( debounceTime(400), distinctUntilChanged(), filter(term => term.length >= 2), switchMap(term => this.productService.search(term)), takeUntil(this.destroy$) ).subscribe(results => this.results = results); Sans ces opérateurs, chaque frappe générerait une requête HTTP. Avec eux, le nombre d'appels est réduit de plus de 90 %. 5. switchMap — Effacer les requêtes précédentes switchMap est le plus utilisé pour les requêtes HTTP en réponse aux événements utilisateur. Lorsqu'un nouveau numéro arrive, il supprime automatiquement l'Observable précédent et s'abonne au nouveau. Cas d'utilisation : navigation dynamique avec les paramètres d'itinéraire this.route.paramMap.pipe( map(params => params.get('id')), filter(id => !!id), switchMap(id => this.articleService.getById(id!)) .subscribe(article => this.article = article); Cas d'utilisation : saisie semi-automatique avec résultats mis à jour // Si l'utilisateur tape "ang" puis "angu" puis "angul", // seules les requêtes pour "angul" sont exécutées searchTerm$.pipe( debounceTime(300), switchMap(term => this.searchService.getSuggestions(term)) .subscribe(suggestions => this.suggestions = suggestions); Règle : utilisez switchMap lorsque seule la dernière réponse est pertinente. 6.
mergeMap — Exécution parallèle Contrairement à switchMap , mergeMap ne supprime PAS les requêtes précédentes : il les exécute toutes en parallèle et génère les résultats au fur et à mesure qu'ils arrivent. Cas d'utilisation : téléchargement de plusieurs fichiers depuis (selectedFiles).pipe( mergeMap(file => this.uploadService.upload(file).pipe( map(result => ({ file: file.name, url: result.url, status: 'ok' })), catchError(err => of({ file: file.name, status: 'error', error: err.message })) )) .subscribe(result => this.uploadResults.push(result)); Attention : utilisez mergeMap(fn, maxConcurrency) pour limiter les requêtes parallèles simultanées. 7. concatMap — Exécution séquentielle garantie concatMap exécute les opérations une par une dans l'ordre, en attendant la fin de la précédente avant de commencer la suivante. Cas d'utilisation : enregistrement séquentiel des enregistrements à partir de([record1, record2, record3]).pipe( concatMap(record => this.db.save(record)) ).subscribe({ next: result => console.log('Saved:', result), complete: () => console.log('Tous les enregistrements enregistrés dans l'ordre !') }); Règle d'or des opérateurs d'ordre supérieur : seul le dernier compte → switchMap Tous en parallèle → mergeMap Un à la fois dans l'ordre → concatMap 8. forkJoin — Attendre plusieurs requêtes parallèles forkJoin est l'équivalent réactif de Promise.all() : il attend que tous les observables se terminent et émet un objet avec les dernières valeurs de chacun. Cas d'utilisation : charger des données à partir de plusieurs points de terminaison pour un tableau de bord ngOnInit() { forkJoin({ user : this.userService.getCurrent(), stats : this.statsService.getDashboard(), notifications : this.notifService.getUnread(), projets : this.projectService.getAll() }).pipe( takeUntil(this.destroy$) ).subscribe({ next: ({ user, stats, notifications,projects }) => { this.user = utilisateur ; this.stats = stats ; this.notifications = this.isReady ;
error = 'Erreur de chargement du tableau de bord' }); } Limitation : si une des sources génère une erreur, forkJoin propage l'erreur et supprime les autres. Utilisez catchError sur chaque observable pour tolérer des échecs partiels. 9. combineLatest — Combinaison de flux en direct Contrairement à forkJoin , combineLatest n'attend pas la fin : il recalcule la sortie à chaque fois qu'une des sources émet une nouvelle valeur. Cas d'utilisation : table filtrée avec plusieurs filtres indépendants combineLatest([ this.searchControl.valueChanges.pipe(startWith('')), this.categoryControl.valueChanges.pipe(startWith('all')), this.statusControl.valueChanges.pipe(startWith('active')) ]).pipe( debounceTime(200), switchMap(([search,category, status]) => this.productService.getFiltered({ search, catégorie, statut }) ), takeUntil(this.destroy$) ).subscribe(products => this.filteredProducts = products); Remarque : startWith() est nécessaire car combineLatest n'émet pas tant que toutes les sources n'ont pas émis au moins une fois. 10. takeUntil — Prévenir les fuites de mémoire Chaque abonnement non géré est une fuite de mémoire potentielle. takeUntil est le modèle standard dans Angular pour terminer automatiquement les abonnements lorsque le composant est détruit. Cas d'utilisation : modèle de nettoyage standard @Component({ ... }) classe d'exportation MyComponent implémente OnInit, OnDestroy { private destroy$ = new Subject(); ngOnInit() { this.dataService.stream$.pipe( takeUntil(this.destroy$) ).subscribe(data => this.data = data); interval(5000).pipe( takeUntil(this.destroy$), switchMap(() => this.statsService.refresh()) ).subscribe(stats => this.stats = stats); } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); } } Alternative moderne (Angular 16+) : utilisez takeUntilDestroyed() depuis @angular/core/rxjs-interop sans gérer manuellement le sujet. 11. catchError — Gestion des erreurs catchError intercepte les erreurs dans le flux et vous permet de récupérer avec une solution de repli observable au lieu d'interrompre l'abonnement.
Cas d'utilisation : secours avec données mises en cache + notification utilisateur this.http.get('/api/products').pipe( catchError(err => { console.error('[ProductService] API Error:', err); this.toastService.showError('Failed toload products. Using cache data.'); return of(this.cacheService.getProducts() ?? []); }) ).subscribe(products => this.products = produits); Cas d'utilisation : nouvelle tentative automatique en cas d'erreur réseau this.http.get('/api/critical-data').pipe( retry({ count: 3, delay: 1000 }), catchError(err => { this.errorService.report(err); return throwError(() => new Error('Service indisponible')); }) ).subscribe(data => this.data = data); 12. shareReplay — Mise en cache et partage de flux shareReplay(1) transforme un observable froid en un observable chaud partagé, avec mise en cache de la dernière valeur. L'appel HTTP n'est exécuté qu'une seule fois quel que soit le nombre d'abonnés. Cas d'utilisation : configuration globale chargée une seule fois @Injectable({ provideIn: 'root' }) export class ConfigService { // L'appel HTTP est exécuté UNE SEULE FOIS, // tous les composants reçoivent la même réponse mise en cache en lecture seule config$ = this.http.get('/api/config').pipe( shareReplay(1) ); constructor(private http: HttpClient) {} } // Dans n'importe quel composant, aucun appel en double @Component({ ... }) export class NavbarComponent { config$ = inject(ConfigService).config$; // utiliser le cache } Résumé : Quand utiliser chaque opérateur Operator Utiliser quand...
Exemple de carte typique Transformer la forme des données Normaliser le filtre API de réponse Ignorer les valeurs non pertinentes Filtrer les événements de frappe du clavier Effet secondaire sans modifier le flux Journalisation, spinner debounceTime Attendre une pause dans la saisie Zone de recherche distinctUntilChanged Éviter les émissions en double Contrôles de formulaire switchMap Seule la dernière requête compte Remplissage automatique, paramètres de route mergeMap Requêtes parallèles sans ordre Téléchargement multiple concatMap Ordre d'exécution garanti Opérations séquentielles forkJoin Attendre N requêtes, toutes terminées Dashboard init combineLatest Plusieurs flux en direct combinés Plusieurs filtres takeUntil Abonnement au nettoyage automatique ngOnDestroy pattern catchError Gérer et récupérer des erreurs Réessayer + repli shareReplay Partager et mettre en cache les flux Global Config Maîtriser ces opérateurs signifie écrire du code angulaire plus lisible, plus efficace et pratiquement exempt de bugs asynchrones. Le secret est de toujours partir de la question : « Que doit-il se passer lorsqu'une nouvelle émission arrive alors que la précédente est encore en vol ? — la réponse vous oriente presque toujours vers le bon opérateur.