RxJS and signals interop

Angular · Advanced · 13 min

Much Angular code and many libraries are RxJS-based; mixing them cleanly with signals is an everyday senior skill.

Signals hold a current value; RxJS Observables model streams of events over time with powerful operators (debounce, switchMap, retry, combine). Angular's @angular/core/rxjs-interop package bridges the two.

  • `toSignal(obs$, { initialValue })` subscribes immediately and exposes the latest value as a signal; it unsubscribes when the component or service is destroyed. Without initialValue the signal is undefined until the first emission; { requireSync: true } is for sources like BehaviorSubject that emit synchronously.
  • `toObservable(sig)` turns a signal into an Observable that emits (asynchronously, via an effect) when the signal changes, so you can apply operators like debounceTime and switchMap.
  • `takeUntilDestroyed()` completes a subscription when the current component or service is destroyed. Call it in an injection context, or pass a DestroyRef.
  • `rxResource({ params, stream })` is the resource API with an Observable loader.
  • `outputFromObservable(obs$)` exposes an Observable as a component output, and outputToObservable converts the other way.

A typical pattern for type-ahead search: signal for the query, toObservable plus debounceTime, distinctUntilChanged and switchMap to the API (cancelling stale requests), then toSignal for the template.

Rule of thumb: use signals for state shown in templates, RxJS for complex event orchestration, and convert at the boundary.

Example

import { Component, inject, signal } from '@angular/core';
import { toObservable, toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { debounceTime, distinctUntilChanged, switchMap, interval } from 'rxjs';

@Component({
  selector: 'app-search', template: `
    <input (input)="query.set($any($event.target).value)" placeholder="Search users" />
    @for (u of results(); track u.id) { <p>{{ u.name }}</p> }`,
})
export class Search {
  private http = inject(HttpClient);
  protected query = signal('');
  protected results = toSignal(
    toObservable(this.query).pipe(
      debounceTime(300),                       // wait for typing to pause
      distinctUntilChanged(),
      switchMap((q) => this.http.get<{ id: number; name: string }[]>(`/api/users?q=${q}`)),
    ),                                         // switchMap cancels stale requests
    { initialValue: [] },
  );
  constructor() {
    interval(60_000).pipe(takeUntilDestroyed()).subscribe(() => console.log('tick')); // auto-unsubscribes
  }
}

Remember

  • toSignal reads an Observable as a signal and unsubscribes automatically
  • toObservable lets you apply RxJS operators to signal changes
  • takeUntilDestroyed() ties subscriptions to the component lifetime

Watch out for

  • Calling toSignal inside a method or event handler, creating a new subscription each time (and failing outside an injection context).

Check yourself

  1. Why use switchMap rather than mergeMap for a type-ahead search request?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: angular.dev/ecosystem/rxjs-interop