Skip to content

switchMap

switchMap() is a higher-order mapping operator. This means it does two things:

  1. It takes a value emitted by the source (outer) Observable.
  2. 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.

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:

  1. fromEvent: Creates the outer Observable from input events.
  2. map: Extracts the text value.
  3. debounceTime(400): Waits for the user to pause typing for 400ms before emitting the term.
  4. distinctUntilChanged(): Prevents searching if the term hasn't changed (e.g., typing "a", deleting "a", typing "a" again quickly).
  5. tap: Used for side effects like logging and setting the loading flag.
  6. switchMap(term => ...): This is the key part.
    • It receives the debounced, distinct search term.
    • If the term is empty, it returns of([]) (an Observable that emits an empty array and completes) to clear results.
    • If the term exists, it returns this.http.get(...) which is the inner Observable.
    • If a new term arrives from distinctUntilChanged before the http.get for the previous term completes, switchMap cancels that pending HTTP request and starts a new one for the new term.
  7. catchError: Handles potential errors during the HTTP request inside switchMap so that an API failure doesn't kill the entire input event stream.
  8. async pipe: In the template (*ngIf="results$ | async as results"), the async pipe subscribes to results$ 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.