switchMap
switchMap()
is a higher-order mapping operator. This means it does two things:
- It takes a value emitted by the source (outer) Observable.
- It uses that value to create and subscribe to a new inner Observable (using a function you provide).
The crucial part is the "switch" behavior:
- If the source Observable emits a new value while a previous inner Observable (created from an earlier source value) is still active (hasn't completed),
switchMap
will immediately unsubscribe from that previous inner Observable. - It then subscribes to the new inner Observable created from the latest source value.
Essentially, switchMap
cancels the previous ongoing inner operation and switches its focus entirely to the new one triggered by the most recent source emission. You only get values from the currently active inner Observable.
Key Characteristics#
- Higher-Order Mapping: Maps values from an outer Observable to inner Observables.
- Switching/Cancellation: Unsubscribes from the previous inner Observable when the outer source emits a new value.
- Focus on Latest: Only emissions from the most recent inner Observable are passed downstream.
- Use Cases: Ideal when you only care about the result corresponding to the latest trigger event and want to discard results from previous, potentially outdated triggers.
Real-World Example Scenario (The Classic Use Case): Type-Ahead Search#
This is the quintessential example for switchMap
. Imagine you have a search input field in your Angular application (perhaps you're building one right now, Monday evening here in Bengaluru!). As the user types, you want to make API calls to fetch search suggestions.
- User types "a" -> Trigger API call for "a"
- User quickly types "n" (now input is "an") -> Trigger API call for "an" ->
switchMap
cancels the pending API call for "a" - User quickly types "g" (now input is "ang") -> Trigger API call for "ang" ->
switchMap
cancels the pending API call for "an"
You only care about the results for the latest search term ("ang"). switchMap
ensures that you don't receive outdated results (like suggestions for "a" arriving after suggestions for "ang") and prevents unnecessary network requests from completing if they've already been superseded.
Code Snippet#
import {
Component,
OnInit,
ViewChild,
ElementRef,
OnDestroy,
} from "@angular/core";
import { HttpClient, HttpParams } from "@angular/common/http";
import { fromEvent, Observable, Subscription, of } from "rxjs";
import {
map,
debounceTime, // Wait for pauses in typing
distinctUntilChanged, // Only search if the term changes
switchMap, // Cancel previous requests, switch to new one
catchError, // Handle HTTP errors
tap, // For logging side-effects
} from "rxjs/operators";
@Component({
selector: "app-typeahead-search",
template: `
<h4>Live Search Example</h4>
<input
#searchInput
type="text"
placeholder="Search Wikipedia..."
class="form-control"
/>
<div *ngIf="loading" class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<ul class="list-group mt-2" *ngIf="results$ | async as results">
<li *ngIf="results.length === 0 && lastSearchTerm && !loading">
No results found for "{{ lastSearchTerm }}"
</li>
<li *ngFor="let result of results" class="list-group-item">
<a [href]="result.link" target="_blank">{{ result.title }}</a>
<p>{{ result.description }}</p>
</li>
</ul>
<div *ngIf="searchError" class="alert alert-danger mt-2">
Error: {{ searchError }}
</div>
`,
})
export class TypeaheadSearchComponent implements OnInit, OnDestroy {
@ViewChild("searchInput", { static: true }) searchInput:
| ElementRef
| undefined;
results$: Observable<any[]> | undefined;
loading = false;
searchError: string | null = null;
lastSearchTerm: string | null = null;
private searchSubscription: Subscription | undefined;
// Using Wikipedia's public API for demonstration
private WIKI_API_URL = "https://en.wikipedia.org/w/api.php";
constructor(private http: HttpClient) {}
ngOnInit(): void {
if (!this.searchInput) return;
// 1. Get the stream of input events from the input element
const inputEvents$ = fromEvent<InputEvent>(
this.searchInput.nativeElement,
"input"
);
this.results$ = inputEvents$.pipe(
// 2. Get the trimmed value from the input event
map((event) => (event.target as HTMLInputElement).value.trim()),
// 3. Wait for 400ms pause in typing before proceeding
debounceTime(400),
// 4. Only proceed if the value has actually changed from the last time
distinctUntilChanged(),
// 5. Show loading indicator and log the term
tap((term) => {
console.log(
`Searching for: "${term}" at ${new Date().toLocaleTimeString()}`
);
this.loading = term.length > 0; // Show loading only if there's a term
this.searchError = null; // Clear previous errors
this.lastSearchTerm = term; // Keep track of the term searched
if (!term) {
// Clear results immediately if input is empty, without API call
return of([]); // Need to return an observable for switchMap
}
return undefined; // Continue the main pipe if term exists
}),
// 6. The core: switchMap! Map the search term to an HTTP request Observable
switchMap((term) => {
if (term.length === 0) {
// If term is empty after debounce/distinct, return observable of empty array
this.loading = false;
return of([]); // 'of' creates an Observable that emits [] and completes
}
// Prepare parameters for Wikipedia API call
const params = new HttpParams()
.set("action", "opensearch")
.set("search", term)
.set("limit", "10") // Limit results
.set("namespace", "0")
.set("format", "json")
.set("origin", "*"); // Needed for CORS in browser
// Return the inner Observable (the HTTP GET request)
// If a new term arrives quickly, switchMap will cancel this HTTP request
// if it's still pending, and start a new one for the new term.
return this.http.get<any[]>(this.WIKI_API_URL, { params }).pipe(
map((response) => {
// Wikipedia API returns [searchTerm, [titles], [descriptions], [links]]
// Let's transform this into a more usable array of objects
const titles = response[1] || [];
const descriptions = response[2] || [];
const links = response[3] || [];
return titles.map((title: string, index: number) => ({
title: title,
description: descriptions[index],
link: links[index],
}));
}),
catchError((err) => {
console.error("API Error:", err);
this.searchError = `Failed to fetch results (${
err.message || "Unknown error"
})`;
this.loading = false;
return of([]); // Return an empty array Observable on error to keep the stream alive
})
);
}),
// 7. Hide loading indicator after results arrive or error handled
tap(() => (this.loading = false))
);
// We can let the async pipe handle the subscription in the template
// this.searchSubscription = this.results$.subscribe(); // Manual subscription not needed for display with async pipe
}
ngOnDestroy(): void {
// Although switchMap handles inner subscriptions, if the component itself
// is destroyed, we should clean up the main subscription to fromEvent
// (if we were subscribing manually). AsyncPipe handles this automatically.
// if (this.searchSubscription) {
// this.searchSubscription.unsubscribe();
// }
console.log("Typeahead search component destroyed.");
}
}
Explanation:
fromEvent
: Creates the outer Observable from input events.map
: Extracts the text value.debounceTime(400)
: Waits for the user to pause typing for 400ms before emitting the term.distinctUntilChanged()
: Prevents searching if the term hasn't changed (e.g., typing "a", deleting "a", typing "a" again quickly).tap
: Used for side effects like logging and setting theloading
flag.switchMap(term => ...)
: This is the key part.- It receives the debounced, distinct search
term
. - If the
term
is empty, it returnsof([])
(an Observable that emits an empty array and completes) to clear results. - If the
term
exists, it returnsthis.http.get(...)
which is the inner Observable. - If a new
term
arrives fromdistinctUntilChanged
before thehttp.get
for the previousterm
completes,switchMap
cancels that pending HTTP request and starts a new one for the newterm
.
- It receives the debounced, distinct search
catchError
: Handles potential errors during the HTTP request insideswitchMap
so that an API failure doesn't kill the entire input event stream.async
pipe: In the template (*ngIf="results$ | async as results"
), theasync
pipe subscribes toresults$
and automatically handles updates and unsubscription when the component is destroyed.
Summary#
switchMap
is your go-to operator when you need to map an event or value to an inner asynchronous operation (like an API call) and you only care about the results of the latest operation, wanting to cancel any previous, now-irrelevant operations.