The exhaustMap
operator in RxJS is a powerful tool for managing scenarios where only one Observable should be active at a time. It is commonly used when handling user actions like button clicks, where you want to ignore subsequent emissions while the current task is still processing.
In this article, we’ll dive into what exhaustMap
is, how it works, its syntax, and practical examples to showcase its usage.
What is exhaustMap
?
The exhaustMap
operator:
- Maps each value from a source Observable to an inner Observable.
- Subscribes to the inner Observable only if there is no currently active subscription.
- Ignores any new emissions from the source Observable while the inner Observable is active.
Key Features
- Single Active Subscription: Ensures only one inner Observable is active at any given time.
- Drop Subsequent Emissions: Ignores new source emissions until the active inner Observable completes.
- Ideal for Debouncing: Particularly useful in user interaction scenarios to avoid processing overlapping actions.
Syntax
typescriptCopy codeexhaustMap(project: (value: T, index: number) => ObservableInput, resultSelector?: (outerValue, innerValue, outerIndex, innerIndex) => any): OperatorFunction
Parameters
project
: A function that maps the source Observable’s value to an inner Observable.- Takes the emitted value and an optional index as arguments.
resultSelector
(optional): A function to combine the outer and inner Observable values.
Returns
- An Observable that emits values from the inner Observable if no other inner Observable is active.
Importing exhaustMap
To use exhaustMap
, import it from rxjs/operators
:
typescriptCopy codeimport { exhaustMap } from 'rxjs/operators';
How exhaustMap
Works
- The source Observable emits a value.
- If there is no active inner Observable,
exhaustMap
maps the emitted value to an inner Observable and subscribes to it. - While the inner Observable is active, any new emissions from the source Observable are ignored.
- Once the inner Observable completes, the next emission from the source Observable is processed.
Examples
Example 1: Ignoring Rapid Clicks
Simulating a button click that triggers an API call, ignoring subsequent clicks until the previous request completes.
typescriptCopy codeimport { fromEvent, of } from 'rxjs';
import { exhaustMap, delay } from 'rxjs/operators';
const button = document.getElementById('action-button');
const clicks$ = fromEvent(button!, 'click');
clicks$
.pipe(
exhaustMap(() => {
console.log('API call started');
return of('API Response').pipe(delay(3000)); // Simulate 3-second API call
})
)
.subscribe((response) => console.log(response));
// Output:
// API call started
// API Response (after 3 seconds)
// (Subsequent clicks during the 3 seconds are ignored)
Explanation
- The first click triggers an API call, represented by a 3-second delayed Observable.
- Any clicks during the active request are ignored until the current Observable completes.
Example 2: Form Submission
Simulating a form submission where only one request is processed at a time.
typescriptCopy codeimport { fromEvent, of } from 'rxjs';
import { exhaustMap, delay } from 'rxjs/operators';
const form = document.getElementById('submit-form');
const formSubmit$ = fromEvent(form!, 'submit');
formSubmit$
.pipe(
exhaustMap(() => {
console.log('Form submitted');
return of('Form processed').pipe(delay(2000)); // Simulate processing time
})
)
.subscribe((result) => console.log(result));
// Output:
// Form submitted
// Form processed (after 2 seconds)
// (Subsequent submissions during processing are ignored)
Explanation
- The first form submission is processed.
- Any submissions while processing is active are ignored.
Example 3: Managing HTTP Requests
Simulating a scenario where multiple HTTP requests could overlap but only the first is processed at a time.
typescriptCopy codeimport { of } from 'rxjs';
import { exhaustMap, delay } from 'rxjs/operators';
const apiRequest$ = of('API Request').pipe(delay(3000)); // Simulate API response
const source$ = of(1, 2, 3, 4);
source$
.pipe(
exhaustMap(() => apiRequest$)
)
.subscribe((response) => console.log(response));
// Output:
// API Request (after 3 seconds)
// (Subsequent emissions are ignored during the active request)
Explanation
- The first emission triggers an HTTP request.
- Any emissions while the request is processing are ignored.
Comparison with Other Operators
exhaustMap
vs mergeMap
mergeMap
: Subscribes to all inner Observables concurrently.exhaustMap
: Subscribes to only one inner Observable at a time, ignoring others.
exhaustMap
vs switchMap
switchMap
: Cancels the active inner Observable when a new value is emitted.exhaustMap
: Ignores new emissions while the active inner Observable is running.
exhaustMap
vs concatMap
concatMap
: Queues new emissions and processes them sequentially.exhaustMap
: Drops new emissions while processing the current one.
Use Cases
- Debouncing User Actions: Prevent duplicate form submissions or rapid button clicks.
- Long-Running Tasks: Ensure only one long-running task (e.g., file upload) is processed at a time.
- Rate-Limiting API Calls: Avoid overlapping HTTP requests that could strain the server.
Best Practices
- Avoid Overuse: Use
exhaustMap
only when it makes sense to drop intermediate emissions. - Error Handling: Combine with
catchError
to handle errors in the inner Observable gracefully. - Use for Non-Critical Emissions: Prefer
exhaustMap
for scenarios where missing emissions is acceptable.
Conclusion
The exhaustMap
operator is a practical choice for scenarios where you want to ensure that only one task or process runs at a time. By ignoring subsequent emissions while an inner Observable is active, exhaustMap
helps manage user interactions and asynchronous workflows efficiently.
Understanding when and how to use exhaustMap
can simplify your code and make your RxJS-based applications more robust. Experiment with it in your projects and see how it helps streamline complex workflows.