RxJS Mapping Operators Explained: map, switchMap, mergeMap, concatMap & exhaustMap
Finally understand RxJS mapping operators! Learn the difference between switchMap, mergeMap, concatMap, and exhaustMap using the Restaurant Analogy, decision trees, and real-world scenarios.
The Restaurant Analogy š½ļø
Imagine you're a waiter at a restaurant. Customers (events) place orders (trigger inner Observables). How you handle multiple orders defines which operator you are!
Meet the Operators
map() - The Simple Transformer
What it does: Transforms each value. No inner Observables involved.
Analogy: š A factory assembly line - each item gets the same transformation.
of(1, 2, 3).pipe(
map(x => x * 10)
);
// Output: 10, 20, 30
switchMap() - The Impatient Waiter
What it does: Cancels previous inner Observable, switches to new one.
Analogy: š½ļø "Forget my pasta order, I want pizza now!" - Only the latest order matters.
searchInput.valueChanges.pipe(
switchMap(term => this.searchService.search(term))
);
ā Best for: Search autocomplete, route changes, polling
mergeMap() - The Multitasking Kitchen
What it does: Runs all inner Observables in parallel.
Analogy: š½ļø "All orders to the kitchen!" - Everyone cooks at once.
from(files).pipe(
mergeMap(file => this.upload(file), 3) // max 3 parallel
);
ā Best for: Bulk uploads, parallel API requests
concatMap() - The One-Chef Kitchen
What it does: Waits for each to complete before starting next.
Analogy: š½ļø "One order at a time, in exact sequence."
from(transactions).pipe(
concatMap(tx => this.bankService.process(tx))
);
ā Best for: Bank transactions, chat messages, sequential saves
exhaustMap() - The Focused Waiter
What it does: Ignores new emissions while current is running.
Analogy: š½ļø "I'm busy with this order, come back later!"
loginButton.clicks.pipe(
exhaustMap(() => this.authService.login())
);
// Rapid clicks are ignored!
ā Best for: Login buttons, form submits, refresh buttons
The Ultimate Decision Tree
Scenario Decision Tree
Quick Scenario Matcher
| Scenario | Operator | Why |
|---|---|---|
| š Search autocomplete | switchMap | Only latest matters |
| š§ Route params change | switchMap | Only current route |
| š¤ Upload multiple files | mergeMap | Parallel is faster |
| š Load dashboard widgets | mergeMap | All load at once |
| š¬ Send chat messages | concatMap | Order preserved |
| š° Bank transactions | concatMap | Sequence critical |
| š Login button | exhaustMap | Prevent double-login |
| š Add to cart click | exhaustMap | Prevent duplicates |
Comparison Cheat Sheet
| Operator | Concurrency | Cancels Prev | Keeps Order | Best For |
|---|---|---|---|---|
| map | N/A | N/A | Yes | Transform |
| switchMap | 1 | ā Yes | Latest only | Search |
| mergeMap | Unlimited | ā No | ā No | Parallel |
| concatMap | 1 (queue) | ā No | ā Yes | Sequential |
| exhaustMap | 1 (ignore) | Ignores | First only | Spam prevention |
Common Mistakes
ā Using mergeMap for search
// Wrong - race conditions!
searchInput.pipe(
mergeMap(term => this.search(term))
);
// Right
searchInput.pipe(
switchMap(term => this.search(term))
);
ā Using switchMap for saves
// Wrong - may cancel saves!
formChanges.pipe(
switchMap(data => this.save(data))
);
// Right
formChanges.pipe(
concatMap(data => this.save(data))
);
Summary: One-Line Definitions
- map: Transform values (no Observables)
- switchMap: Cancel old, use new (search)
- mergeMap: Run all parallel (uploads)
- concatMap: One at a time, in order (transactions)
- exhaustMap: Ignore new while busy (submit buttons)