typescript xml 100 lines · 3 tabs

Debounced Typeahead Search in Angular with switchMap and Cancellation

Shared by codesnips Jul 2026
3 tabs
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { catchError } from 'rxjs/operators';

export interface SearchResult {
  id: string;
  label: string;
  sublabel?: string;
}

@Injectable({ providedIn: 'root' })
export class SearchService {
  private readonly endpoint = '/api/search';

  constructor(private http: HttpClient) {}

  search(term: string): Observable<SearchResult[]> {
    const trimmed = term.trim();
    if (!trimmed) {
      return of([]);
    }

    const params = new HttpParams().set('q', trimmed).set('limit', '8');

    return this.http.get<SearchResult[]>(this.endpoint, { params }).pipe(
      catchError(() => of([]))
    );
  }
}
3 files · typescript, xml Explain with highlit

This snippet shows the canonical way to build a typeahead search in Angular using a reactive form control, RxJS operators, and an HTTP-backed service. The core problem is that a naive keyup handler fires a request on every keystroke, which floods the backend and produces flickering, out-of-order results. The combination of debounceTime, distinctUntilChanged, and switchMap solves all three issues at once.

In search.service.ts, SearchService exposes a single search method that returns a cold Observable of SearchResult[]. It short-circuits blank terms with of([]) so no HTTP call is made for empty input, and it uses catchError to swallow transient failures and keep the outer stream alive — a request error should degrade to an empty list, not tear down the whole typeahead. Returning an observable rather than a promise is what lets the component cancel in-flight work.

In typeahead-search.component.ts, the searchControl is a plain FormControl, and its valueChanges stream is the input to the pipeline. debounceTime(300) waits for a pause in typing before emitting, so bursts of keystrokes collapse into one term. distinctUntilChanged drops emissions where the term is unchanged, avoiding redundant calls when the value settles back to a prior string. The crucial operator is switchMap: each new term unsubscribes from the previous inner search observable, which cancels the outstanding HTTP request and guarantees that only the latest query's results ever reach the view. This is what prevents the classic race where a slow response for an old term overwrites a fresh one.

The results$ stream also toggles a loading flag via tap, and everything is rendered through the async pipe in typeahead-search.component.html, so there is no manual subscription to leak and no ngOnDestroy teardown to write. The template guards on loading and an empty-but-searched state to show sensible feedback. Reaching for this pattern makes sense whenever remote lookups are driven by user input; the main trade-off is choosing a debounce interval that balances responsiveness against request volume.


Related snips

Share this code

Here's the card — post it anywhere.

Debounced Typeahead Search in Angular with switchMap and Cancellation — share card
Link copied