๐Ÿณ IntuitiveFE
Login
โ† All concepts

Angular RxJS Operators

โฑ๏ธ ~6-minute bite ยท solve the sandbox to master

0%lesson
๐Ÿง’

5-Year-Old Metaphor

โ€” The physical, real-world picture. No jargon.

๐Ÿšฆ The flattening operators all do the same thing โ€” take an outer observable and map each value to an inner observable โ€” but they differ in how they handle overlap.

๐Ÿ”€ switchMap

Last request wins

Search autocomplete โ€” old results thrown out when you keep typing. Stale responses are irrelevant.

๐Ÿ›‘ exhaustMap

First click wins

Submit button โ€” clicking 5 times only fires once. Duplicate is worse than stale.

๐Ÿ“‹ concatMap

Take a number

Serial uploads โ€” each waits its turn. Order and completeness both matter.

๐Ÿ”€ mergeMap

Open all counters

Parallel requests โ€” all go at once, results arrive as they complete.

๐Ÿ”€ combineLatest

All lanes merge

Multi-source dashboard โ€” only emit when all sources have data, react when any changes.

๐ŸŽ›๏ธ

Interactive Sandbox

โ€” Move something, see it react instantly.

Operator

Last request wins โ€” cancels previous inner observable

๐Ÿง  Last request wins: search autocomplete โ€” old results thrown out when you keep typing.

Emission simulator โ€” click emit to fire the outer observable

Click โ€œEmit!โ€ to simulate outer observable emissions

activecancelledqueued

When to use

When new outer values make previous inner observables stale. Search autocomplete: only the latest search result matters. Route params: only the current route data matters.

switchMap โ€” real-world usage
ts
1import { Component, OnInit } from '@angular/core';
2import { FormControl, ReactiveFormsModule } from '@angular/forms';
3import { switchMap, debounceTime, distinctUntilChanged } from 'rxjs/operators';
4import { HttpClient } from '@angular/common/http';
5ย 
6@Component({
7 standalone: true,
8 imports: [ReactiveFormsModule],
9 template: `<input [formControl]="searchCtrl" placeholder="Search..." />`,
10})
11export class SearchComponent implements OnInit {
12 searchCtrl = new FormControl('');
13 results: string[] = [];
14ย 
15 constructor(private http: HttpClient) {}
16ย 
17 ngOnInit() {
18 this.searchCtrl.valueChanges.pipe(
19 debounceTime(300),
20 distinctUntilChanged(),
21 // switchMap: when a new value arrives, CANCEL the previous HTTP call
22 // The old XHR is aborted โ€” no stale results overwriting newer ones
23 switchMap((query) => this.http.get<string[]>(`/api/search?q=${query}`)),
24 ).subscribe((results) => {
25 this.results = results;
26 });
27 }
28}
29ย 
30// โ”€โ”€ Route params (Angular Router pattern) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
31import { ActivatedRoute } from '@angular/router';
32ย 
33@Component({ template: '' })
34export class ArticleComponent implements OnInit {
35 constructor(private route: ActivatedRoute, private http: HttpClient) {}
36ย 
37 ngOnInit() {
38 this.route.paramMap.pipe(
39 switchMap((params) => {
40 const id = params.get('id');
41 // Navigating to a new article cancels the old article's HTTP request
42 return this.http.get(`/api/articles/${id}`);
43 }),
44 ).subscribe((article) => { /* render */ });
45 }
46}
Gotcha: switchMap cancels the previous inner observable โ€” but cancelling an HTTP observable only cancels the subscription, not the server-side work. The server still processes the request. For truly cancellable requests, use AbortController.
vs others: vs exhaustMap: switchMap = last wins (search), exhaustMap = first wins (submit). vs mergeMap: switchMap cancels old, mergeMap keeps all concurrent.
Explored 1/5:๐Ÿ”€ switchMap๐Ÿ”€ mergeMap๐Ÿ›‘ exhaustMap๐Ÿ“‹ concatMap๐Ÿ”€ combineLatest
๐ŸŽฏ

Challenge

Explore all 5 RxJS operators. Use the emit simulator to see how each handles rapid emissions.

Try it
๐ŸŽฏ

Why Should I Care?

โ€” The exact interview question + the bug it kills.

Interview questions

Q: When should you use switchMap vs exhaustMap?

switchMap for search (stale results are bad โ€” keep only the latest). exhaustMap for submit (duplicates are bad โ€” ignore clicks while processing). The mental model: switchMap = โ€œlast request winsโ€, exhaustMap = โ€œfirst click wins, ignore the rest until doneโ€.

Q: What's the memory leak risk with mergeMap?

mergeMap has no concurrency limit by default โ€” if the source emits 100 values, mergeMap creates 100 simultaneous inner subscriptions. Each subscription holds memory until it completes. Always use takeUntilDestroyed() to cap the subscription lifetime, and consider the concurrency parameter mergeMap(fn, N) to limit simultaneous inner subscriptions.

Q: Why is combineLatest useful with form controls?

Multi-field validation requires knowing the current state of ALL fields simultaneously. combineLatest emits whenever any field changes and provides the latest value from every field. This lets you write validation logic that considers all fields together โ€” checking that password and confirmPassword match, or that email is valid before enabling the submit button.

๐Ÿ”ฌ

The Deep Dive

โ€” Spec refs, engine internals, the minutiae.

takeUntilDestroyed โ€” the modern cleanup pattern

Angular 16+ ships takeUntilDestroyed() in @angular/core/rxjs-interop. It automatically unsubscribes when the component is destroyed โ€” no more Subject + takeUntil + ngOnDestroy ceremony.

js
1import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
2ย 
3@Component({ standalone: true, template: '' })
4export class MyComponent {
5 private destroyRef = inject(DestroyRef);
6ย 
7 ngOnInit() {
8 this.someService.data$.pipe(
9 switchMap((query) => this.http.get(`/api/search?q=${query}`)),
10 takeUntilDestroyed(this.destroyRef), // auto-unsubscribe on destroy
11 ).subscribe((results) => { /* ... */ });
12 }
13}
14ย 
15// Even simpler โ€” inject DestroyRef implicitly in the injection context:
16export class CleanComponent {
17 constructor() {
18 interval(1000).pipe(
19 takeUntilDestroyed(), // no argument โ€” uses current injection context
20 ).subscribe(console.log);
21 }
22}

Hot vs cold observables

Cold observables start producing when subscribed โ€” each subscriber gets its own execution (HTTP calls are cold: each subscribe = new XHR). Hot observables produce regardless of subscribers โ€” events, Subjects, WebSockets. Understanding this matters for sharing: two components subscribing to http.get() each fire a separate HTTP request. Use shareReplay(1) to multicast.

js
1// Without shareReplay โ€” 2 HTTP requests if 2 components subscribe
2const user$ = this.http.get('/api/me');
3ย 
4// With shareReplay(1) โ€” 1 HTTP request, result shared + replayed
5const user$ = this.http.get('/api/me').pipe(
6 shareReplay(1), // cache latest value, replay to new subscribers
7);

catchError + EMPTY โ€” graceful error handling

js
1import { catchError, EMPTY, of } from 'rxjs';
2ย 
3// catchError swallows the error and returns a fallback observable
4// EMPTY completes without emitting โ€” the stream continues
5this.search$.pipe(
6 switchMap((query) =>
7 this.http.get(`/api/search?q=${query}`).pipe(
8 catchError((err) => {
9 console.error('Search failed:', err);
10 return EMPTY; // swallow error, don't kill the outer stream
11 // OR: return of([]); // emit empty results
12 }),
13 ),
14 ),
15).subscribe((results) => { /* ... */ });
16ย 
17// Without inner catchError: one HTTP error kills the entire subscription
18// and search stops working for the rest of the session
๐ŸŽค

Interview Questions

โ€” Real questions from real interviews โ€” with answers.

switchMap = last wins (search autocomplete); exhaustMap = first wins (form submit).

mergeMap has no concurrency limit by default โ€” N rapid emissions = N simultaneous subscriptions.

combineLatest doesn't emit until ALL source observables have emitted at least once. Use startWith() to seed each source.

Cold: each subscriber gets its own execution (HTTP = new XHR per subscribe). Hot: shared execution regardless of subscribers.

Catch inside the inner observable so errors are contained; the outer subscription survives.

Replaces the Subject + takeUntil + ngOnDestroy pattern โ€” automatically unsubscribes when the injection context is destroyed.

๐ŸŽฎ

Memory Game

โ€” Quick quiz โ€” lock the concept in long-term memory.
1/4

What does shareReplay(1) do that share() does not?