Date: June 10, 2022
Introduction
RxJS is a powerful reactive programming library, and Angular heavily relies on it for handling asynchronous data streams. While switchMap
is the most popular operator for flattening observables, there are other equally important operators like exhaustMap
, mergeMap
, and concatMap
that can solve specific use cases elegantly.
In this post, we’ll dive into these operators, explore their differences, common pitfalls, and practical Angular patterns to help you choose the right one for your scenario.
Quick Recap: What is switchMap
?
switchMap
maps to a new observable and cancels the previous one if it hasn’t completed yet.
Example:
searchTerm$.pipe(
switchMap(term => this.api.search(term))
).subscribe(results => {
this.results = results;
});
Good for user input scenarios where you want to discard previous requests when new ones come in (e.g., live search).
1. exhaustMap
: Ignoring New Inner Observables While One is Active
exhaustMap
ignores new emissions while the current inner observable is still running.
Use case: Prevent duplicate HTTP requests on button clicks
Example:
saveClicks$.pipe(
exhaustMap(() => this.api.saveData())
).subscribe(() => {
console.log('Save complete');
});
Here, if the user clicks the save button multiple times quickly, only the first click triggers the save API call until it completes; subsequent clicks are ignored until then.
Pitfall: Be careful not to block new actions forever if the inner observable never completes.
2. mergeMap
: Running All Inner Observables Concurrently
mergeMap
subscribes to all inner observables and merges their outputs concurrently.
Use case: Parallel API calls
Example:
userIds$.pipe(
mergeMap(id => this.api.getUser(id))
).subscribe(user => {
console.log('Fetched user:', user);
});
This will trigger all user fetches concurrently, and the results arrive as they complete.
Pitfall: Too many concurrent inner subscriptions can overwhelm resources. Use mergeMap
’s concurrency parameter to limit parallelism:
mergeMap(id => this.api.getUser(id), 3) // Max 3 concurrent requests
3. concatMap
: Queuing Inner Observables Sequentially
concatMap
queues the inner observables and subscribes to them one after another, waiting for each to complete before starting the next.
Use case: Ensuring ordered execution
Example:
clicks$.pipe(
concatMap(() => this.api.saveData())
).subscribe(() => {
console.log('Data saved sequentially');
});
This ensures that save requests do not overlap and happen in the exact order of clicks.
Choosing the Right Operator
Operator | Behavior | Common Use Case |
---|---|---|
switchMap | Cancels previous inner observable | Autocomplete, live search |
exhaustMap | Ignores new emissions while busy | Prevent duplicate form submissions |
mergeMap | Runs all inner observables concurrently | Parallel API calls |
concatMap | Queues inner observables, runs sequentially | Ordered execution, chained requests |
Real-World Angular Pattern Example
Imagine a form where users can save drafts multiple times but want to avoid overlapping saves:
saveClicks$ = new Subject<void>();
constructor(private api: ApiService) {
this.saveClicks$.pipe(
exhaustMap(() => this.api.saveDraft())
).subscribe(() => {
this.snackBar.open('Draft saved');
});
}
// Bind button to call this.saveClicks$.next()
This prevents double saves if the user clicks too fast.
Summary
switchMap
: Use when you want to cancel previous async work.exhaustMap
: Use when you want to ignore new triggers until current finishes.mergeMap
: Use when you want to run all concurrently.concatMap
: Use when you want to queue and run sequentially.
Conclusion
Mastering these RxJS operators gives you fine-grained control over asynchronous flows in Angular apps. Experiment with each in your projects to understand which fits your scenarios best.