Angular course · Module 7: RxJS

Practical RxJS Patterns in Angular

2 min read ·
In this lesson5

Search with Debounce

@Component({
  template: `
    <input #searchInput placeholder="Search for a samurai...">
    @for (result of searchResults$ | async; track result.id) {
      <div>{{ result.name }}</div>
    }
  `
})
export class SearchComponent implements AfterViewInit {
  @ViewChild('searchInput') searchInput!: ElementRef;
  searchResults$!: Observable<Samurai[]>;

  private http = inject(HttpClient);

  ngAfterViewInit(): void {
    this.searchResults$ = fromEvent<InputEvent>(
      this.searchInput.nativeElement,
      'input'
    ).pipe(
      map(event => (event.target as HTMLInputElement).value),
      debounceTime(300),           // Wait 300ms
      distinctUntilChanged(),       // Ignore the same values
      filter(query => query.length >= 2),  // Min 2 characters
      switchMap(query =>            // Cancel previous query
        this.http.get<Samurai[]>(`/api/search?q=${query}`).pipe(
          catchError(() => of([]))  // Error handling
        )
      )
    );
  }
}

Polling with Retry

@Injectable({ providedIn: 'root' })
export class StatusService {
  private http = inject(HttpClient);

  getStatusWithPolling(): Observable<Status> {
    return interval(5000).pipe(  // Every 5 seconds
      startWith(0),              // Immediately on start
      switchMap(() =>
        this.http.get<Status>('/api/status').pipe(
          retry({
            count: 3,
            delay: 1000  // Retry 3 times every second
          }),
          catchError(error => {
            console.error('Status check failed:', error);
            return of({ status: 'unknown', error: true });
          })
        )
      ),
      shareReplay(1)  // Share result between subscribers
    );
  }
}

Loading with Cache

@Injectable({ providedIn: 'root' })
export class CachedDataService {
  private http = inject(HttpClient);
  private cache$ = new Map<string, Observable<any>>();

  getData<T>(url: string): Observable<T> {
    if (!this.cache$.has(url)) {
      this.cache$.set(url,
        this.http.get<T>(url).pipe(
          shareReplay({ bufferSize: 1, refCount: true }),
          tap({
            error: () => this.cache$.delete(url)
          })
        )
      );
    }
    return this.cache$.get(url)!;
  }

  invalidateCache(url: string): void {
    this.cache$.delete(url);
  }
}

Handling Multiple Dependent Requests

getSamuraiWithWeapons(id: number): Observable<SamuraiWithWeapons> {
  return this.http.get<Samurai>(`/api/samurai/${id}`).pipe(
    switchMap(samurai =>
      forkJoin({
        weapons: this.http.get<Weapon[]>(`/api/samurai/${id}/weapons`),
        skills: this.http.get<Skill[]>(`/api/samurai/${id}/skills`)
      }).pipe(
        map(({ weapons, skills }) => ({
          ...samurai,
          weapons,
          skills
        }))
      )
    )
  );
}

Signals vs RxJS - When to Use Which?

Use CaseSignalsRxJS
Component stateyesno
Simple derived calculationsyes, computed()no
HTTP requestsnoyes
Debounce/throttlenoyes
Multiple transformationsnoyes
Combining streamsnoyes
// Integration - the best of both worlds
@Component({...})
export class HybridComponent {
  private http = inject(HttpClient);

  // Signal for UI state
  isLoading = signal(false);
  selectedId = signal<number | null>(null);

  // RxJS for HTTP
  samurai$ = toObservable(this.selectedId).pipe(
    filter(id => id !== null),
    tap(() => this.isLoading.set(true)),
    switchMap(id => this.http.get<Samurai>(`/api/samurai/${id}`)),
    tap(() => this.isLoading.set(false))
  );

  // Observable → Signal
  samurai = toSignal(this.samurai$);
}

Docs entries used in this lesson

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. When does forkJoin emit a value?

  2. 2. What is the shareReplay() operator used for in practical RxJS patterns?

These are 2 of 4 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Code editor

    Complete the code that sends three HTTP requests in parallel and reacts only when all of them have completed.

  • Vertical ordering

    Arrange the operators of a full search chain with debounce in their order inside pipe():

  • Horizontal ordering

    Arrange the conversion of an Observable into a Signal: samurai = toSignal(this.samurai$)

  • Click in order

    Arrange the call that converts a Signal into an Observable: toObservable(this.selectedId)

Useful articles