Cours Angular · Module 7: RxJS

Les opérateurs RxJS - Les techniques ninja

2 min de lecture
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. 1. Quelle est la principale différence entre switchMap et mergeMap ?

  2. 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(...))

Articles utiles