Cours Angular · Module 7: RxJS
Les opérateurs RxJS - Les techniques ninja
Dans cette leçon3
Les opérateurs sont des fonctions pures qui transforment les flux - comme différentes techniques ninja changeant la façon dont l'information est transmise.
Opérateurs de transformation
1import { map, switchMap, mergeMap, concatMap, exhaustMap } from 'rxjs/operators';
2
3// map - transforme chaque élément
4source$.pipe(
5 map(value => value * 2)
6);
7
8// switchMap - bascule vers un nouveau flux, annule le précédent
9// Idéal pour la recherche (annuler la requête précédente)
10searchInput$.pipe(
11 switchMap(query => this.http.get(`/search?q=${query}`))
12);
13
14// mergeMap - exécution en parallèle de tout
15// Utile quand l'ordre n'a pas d'importance
16ids$.pipe(
17 mergeMap(id => this.http.get(`/item/${id}`))
18);
19
20// concatMap - séquentiel, préserve l'ordre
21// Quand l'ordre est important
22actions$.pipe(
23 concatMap(action => this.saveAction(action))
24);
25
26// exhaustMap - ignore les nouveaux jusqu'à ce que le courant se termine
27// Idéal pour les boutons de sauvegarde (empêcher les doubles clics)
28saveButton$.pipe(
29 exhaustMap(() => this.saveData())
30);Opérateurs de filtrage
1import { filter, take, takeUntil, takeWhile, distinctUntilChanged, debounceTime } from 'rxjs/operators';
2
3// filter - ne laisse passer que les éléments correspondants
4numbers$.pipe(
5 filter(n => n > 5)
6);
7
8// take - prend les n premiers éléments
9numbers$.pipe(
10 take(3) // Seulement les 3 premiers
11);
12
13// distinctUntilChanged - supprime les doublons consécutifs
14values$.pipe(
15 distinctUntilChanged() // [1,1,2,2,3,1] -> [1,2,3,1]
16);
17
18// debounceTime - attend une pause dans les émissions
19searchInput$.pipe(
20 debounceTime(300) // Attend 300ms sans nouvelle valeur
21);Opérateurs de combinaison
1import { combineLatest, forkJoin, merge, concat, zip } from 'rxjs';
2
3// combineLatest - émet dès que l'un des flux émet (après les premières émissions)
4combineLatest([user$, settings$]).subscribe(([user, settings]) => {
5 console.log(user, settings);
6});
7
8// forkJoin - attend que tout se termine (comme Promise.all)
9forkJoin([
10 this.http.get('/api/samurai'),
11 this.http.get('/api/weapons')
12]).subscribe(([samurai, weapons]) => {
13 // Les deux sont terminés
14});
15
16// merge - combine des flux en parallèle
17merge(clicks$, keyups$).subscribe(event => {
18 console.log('Event:', event);
19});Code de cette leçon : src/app/app.component.ts
1import { Component, signal, OnDestroy } from '@angular/core';
2import { CommonModule } from '@angular/common';
3import { Observable, Subject, of, from, timer, combineLatest, forkJoin } from 'rxjs';
4import { map, delay, takeUntil, mergeMap, switchMap, concatMap, exhaustMap, catchError } from 'rxjs/operators';
5
6@Component({
7 selector: 'app-root',
8 standalone: true,
9 imports: [CommonModule],
10 template: `
11 <div class="operators-dojo">
12 <h1>Dojo des opérateurs RxJS</h1>
13
14 <section class="transformation">
15 <h2>Transformation (map, mergeMap, switchMap)</h2>
16
17 <div class="demo-box">
18 <h3>Map - transformation des valeurs</h3>
19 <button (click)="demoMap()">Lancer Map</button>
20 <div class="result">{{ mapResult() }}</div>
21 </div>
22
23 <div class="demo-box">
24 <h3>MergeMap - tout en parallèle</h3>
25 <button (click)="demoMergeMap()">Lancer MergeMap</button>
26 <div class="result">
27 @for (item of mergeMapResults(); track $index) {
28 <span class="tag">{{ item }}</span>
29 }
30 </div>
31 </div>
32
33 <div class="demo-box">
34 <h3>SwitchMap - annule le précédent</h3>
35 <button (click)="demoSwitchMap()">Clique vite !</button>
36 <div class="result">
37 Clics : {{ switchClickCount() }}<br>
38 Résultat : {{ switchMapResult() }}
39 </div>
40 </div>
41 </section>
42
43 <section class="combination">
44 <h2>Combinaison (combineLatest, forkJoin)</h2>
45
46 <div class="demo-box">
47 <h3>CombineLatest - la dernière valeur de chacun</h3>
48 <div class="sliders">
49 <label>
50 Force : {{ strength() }}
51 <input type="range" min="1" max="100" [value]="strength()"
52 (input)="onStrengthChange($event)">
53 </label>
54 <label>
55 Agilité : {{ agility() }}
56 <input type="range" min="1" max="100" [value]="agility()"
57 (input)="onAgilityChange($event)">
58 </label>
59 </div>
60 <div class="result">
61 Puissance totale : {{ combinedPower() }}
62 </div>
63 </div>
64
65 <div class="demo-box">
66 <h3>ForkJoin - attendre tous les flux</h3>
67 <button (click)="demoForkJoin()" [disabled]="forkJoinLoading()">
68 {{ forkJoinLoading() ? 'Chargement...' : 'Récupérer toutes les données' }}
69 </button>
70 <div class="result">
71 @if (forkJoinResult()) {
72 <div>Samouraïs : {{ forkJoinResult()?.samurai }}</div>
73 <div>Armes : {{ forkJoinResult()?.weapons }}</div>
74 <div>Missions : {{ forkJoinResult()?.missions }}</div>
75 }
76 </div>
77 </div>
78 </section>
79
80 <section class="error-handling">
81 <h2>Gestion des erreurs (catchError)</h2>
82
83 <div class="demo-box">
84 <h3>CatchError - défense contre les erreurs</h3>
85 <button (click)="demoWithError()">Simuler une erreur d’API</button>
86 <div class="result" [class.error]="hasError()">
87 {{ errorResult() }}
88 </div>
89 </div>
90 </section>
91 </div>
92 `,
93 styles: [`
94 .operators-dojo {
95 font-family: 'Segoe UI', sans-serif;
96 padding: 20px;
97 background: linear-gradient(135deg, #2d1b4e 0%, #1a1a2e 100%);
98 min-height: 100vh;
99 color: #eee;
100 }
101
102 h1 {
103 text-align: center;
104 color: #a855f7;
105 }
106
107 h2 {
108 background: #a855f7;
109 color: white;
110 padding: 10px;
111 border-radius: 5px;
112 }
113
114 section {
115 margin: 20px 0;
116 }
117
118 .demo-box {
119 background: rgba(255,255,255,0.1);
120 padding: 15px;
121 border-radius: 10px;
122 margin: 10px 0;
123 }
124
125 .demo-box h3 {
126 margin-top: 0;
127 color: #a855f7;
128 }
129
130 button {
131 padding: 10px 20px;
132 background: #a855f7;
133 color: white;
134 border: none;
135 border-radius: 5px;
136 cursor: pointer;
137 margin: 5px 0;
138 }
139
140 button:hover:not(:disabled) {
141 background: #c084fc;
142 }
143
144 button:disabled {
145 opacity: 0.5;
146 cursor: not-allowed;
147 }
148
149 .result {
150 background: rgba(0,0,0,0.3);
151 padding: 10px;
152 border-radius: 5px;
153 margin-top: 10px;
154 min-height: 30px;
155 }
156
157 .result.error {
158 border: 2px solid #ef4444;
159 color: #fca5a5;
160 }
161
162 .tag {
163 display: inline-block;
164 background: #a855f7;
165 padding: 3px 8px;
166 border-radius: 3px;
167 margin: 2px;
168 font-size: 0.9em;
169 }
170
171 .sliders {
172 display: flex;
173 flex-direction: column;
174 gap: 10px;
175 }
176
177 .sliders label {
178 display: flex;
179 align-items: center;
180 gap: 10px;
181 }
182
183 .sliders input[type="range"] {
184 flex: 1;
185 }
186 `]
187})
188export class AppComponent implements OnDestroy {
189 private destroy$ = new Subject<void>();
190 private switchClick$ = new Subject<void>();
191
192 // Démo de map
193 mapResult = signal('');
194
195 // Démo de mergeMap
196 mergeMapResults = signal<string[]>([]);
197
198 // Démo de switchMap
199 switchClickCount = signal(0);
200 switchMapResult = signal('');
201
202 // Démo de combineLatest
203 strength = signal(50);
204 agility = signal(50);
205 combinedPower = signal(100);
206
207 // Démo de forkJoin
208 forkJoinLoading = signal(false);
209 forkJoinResult = signal<any>(null);
210
211 // Démo de la gestion des erreurs
212 hasError = signal(false);
213 errorResult = signal('Clique sur le bouton pour tester');
214
215 constructor() {
216 // SwitchMap - annule la requête précédente
217 this.switchClick$.pipe(
218 takeUntil(this.destroy$),
219 switchMap(() => {
220 const clickNum = this.switchClickCount();
221 return timer(2000).pipe(
222 map(() => `Terminé pour le clic #${clickNum}`)
223 );
224 })
225 ).subscribe(result => this.switchMapResult.set(result));
226 }
227
228 ngOnDestroy(): void {
229 this.destroy$.next();
230 this.destroy$.complete();
231 }
232
233 demoMap(): void {
234 const samurai = ['Musashi', 'Kojiro', 'Jubei'];
235
236 from(samurai).pipe(
237 map(name => `Samouraï : ${name.toUpperCase()}`)
238 ).subscribe({
239 next: val => this.mapResult.set(val),
240 complete: () => this.mapResult.update(v => v + '')
241 });
242 }
243
244 demoMergeMap(): void {
245 this.mergeMapResults.set([]);
246
247 const missions = ['Reconnaissance', 'Défense', 'Attaque'];
248
249 from(missions).pipe(
250 mergeMap(mission =>
251 of(`${mission} terminée`).pipe(
252 delay(Math.random() * 2000)
253 )
254 )
255 ).subscribe(result => {
256 this.mergeMapResults.update(arr => [...arr, result]);
257 });
258 }
259
260 demoSwitchMap(): void {
261 this.switchClickCount.update(c => c + 1);
262 this.switchMapResult.set('Traitement en cours...');
263 this.switchClick$.next();
264 }
265
266 onStrengthChange(event: Event): void {
267 const value = +(event.target as HTMLInputElement).value;
268 this.strength.set(value);
269 this.updateCombinedPower();
270 }
271
272 onAgilityChange(event: Event): void {
273 const value = +(event.target as HTMLInputElement).value;
274 this.agility.set(value);
275 this.updateCombinedPower();
276 }
277
278 private updateCombinedPower(): void {
279 this.combinedPower.set(this.strength() + this.agility());
280 }
281
282 demoForkJoin(): void {
283 this.forkJoinLoading.set(true);
284 this.forkJoinResult.set(null);
285
286 forkJoin({
287 samurai: of(5).pipe(delay(1000)),
288 weapons: of(12).pipe(delay(1500)),
289 missions: of(3).pipe(delay(800))
290 }).subscribe(result => {
291 this.forkJoinResult.set(result);
292 this.forkJoinLoading.set(false);
293 });
294 }
295
296 demoWithError(): void {
297 this.hasError.set(false);
298 this.errorResult.set('Envoi de la requête...');
299
300 const failingApi$ = new Observable(subscriber => {
301 setTimeout(() => {
302 subscriber.error(new Error('Serveur indisponible !'));
303 }, 1000);
304 });
305
306 failingApi$.pipe(
307 catchError(error => {
308 this.hasError.set(true);
309 return of(`Erreur interceptée : ${error.message} - données de secours utilisées`);
310 })
311 ).subscribe(result => this.errorResult.set(result as string));
312 }
313}Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Quelle est la principale différence entre switchMap et mergeMap ?
2. Que fait l'opérateur map() dans RxJS ?
Voici 2 des 5 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Tri horizontal
Ordonne l'import des opérateurs de transformation depuis le package d'opérateurs rxjs :
- Tri vertical
Ordonne les lignes de la chaîne d'opérateurs de recherche dans l'ordre où on les écrit dans pipe() :
- Éditeur de code
Complète le pipe du flux de recherche pour qu'il ne laisse passer que les expressions d'au moins 3 caractères.
- Clique dans l'ordre
Ordonne l'appel d'opérateur sur un flux : source$.pipe(map(...))