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
initialValuethe signal isundefineduntil the first emission;{ requireSync: true }is for sources likeBehaviorSubjectthat 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
debounceTimeandswitchMap. - `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
outputToObservableconverts 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
toSignalreads an Observable as a signal and unsubscribes automaticallytoObservablelets you apply RxJS operators to signal changestakeUntilDestroyed()ties subscriptions to the component lifetime
Watch out for
- Calling
toSignalinside a method or event handler, creating a new subscription each time (and failing outside an injection context).