CPCODELAB
Angular

Essential RxJS Operators

12 min read

The Operators You Will Use Most

RxJS ships with over 100 operators but a handful cover 90% of real-world Angular needs. Understanding when to use which flattening operator is the single biggest RxJS skill to develop.

Transformation: map and filter

ts
import { of } from 'rxjs';
import { map, filter } from 'rxjs/operators';

const prices$ = of(10, 25, 5, 40, 3);

prices$
  .pipe(
    filter(price => price >= 10),   // keep only prices >= 10
    map(price => price * 1.1)        // apply 10% markup
  )
  .subscribe(console.log);
// 11, 27.5, 44

Flattening: switchMap, mergeMap, concatMap, exhaustMap

  • `switchMap` — cancels the previous inner Observable on each new emission. Use for live search, route-driven data fetching.
  • `mergeMap` — runs all inner Observables concurrently. Use for parallel fire-and-forget operations.
  • `concatMap` — queues inner Observables one after another. Use when order matters (e.g. sequential API calls).
  • `exhaustMap` — ignores new emissions while an inner Observable is active. Use for login button clicks to prevent double-submit.
ts
import { Component, OnInit, inject } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { AsyncPipe } from '@angular/common';
import {
  Observable, of,
  debounceTime, distinctUntilChanged, switchMap, catchError
} from 'rxjs';

interface Repo { id: number; full_name: string; stargazers_count: number; }

@Component({
  selector: 'app-repo-search',
  standalone: true,
  imports: [ReactiveFormsModule, AsyncPipe],
  template: `
    <input [formControl]="query" placeholder="Search GitHub repos" />
    <ul>
      @for (repo of repos$ | async; track repo.id) {
        <li>{{ repo.full_name }} ({{ repo.stargazers_count }} stars)</li>
      }
    </ul>
  `
})
export class RepoSearchComponent implements OnInit {
  private http = inject(HttpClient);
  query = new FormControl('');
  repos$!: Observable<Repo[]>;

  ngOnInit() {
    this.repos$ = this.query.valueChanges.pipe(
      debounceTime(350),
      distinctUntilChanged(),
      switchMap(term =>
        term
          ? this.http
              .get<{ items: Repo[] }>(`https://api.github.com/search/repositories?q=${term}&per_page=5`)
              .pipe(map(r => r.items), catchError(() => of([])))
          : of([])
      )
    );
  }
}

debounceTime and distinctUntilChanged

debounceTime(ms) waits until no new value arrives for ms milliseconds before emitting — preventing a network call on every keystroke. distinctUntilChanged() skips emission when the new value equals the previous one — avoiding duplicate requests when the user types then backtracks.

Always pair debounceTime + distinctUntilChanged + switchMap for typeahead/live-search patterns. This trio is so common it has become an Angular idiom.

Ready to test yourself?
Practice Angular— quiz & coding exercises