Interop between signals & RxJS
Key takeaway —
toSignal()turns an Observable into a signal (subscription managed automatically),toObservable()does the reverse. Signals and RxJS are complementary: signals for synchronous state, RxJS for asynchronous streams.
Signals do not replace RxJS — they complement it. A real application combines the two: RxJS to orchestrate the asynchronous (HTTP, websockets, debounced events), signals for the state that drives the display. The bridge between the two worlds is in @angular/core/rxjs-interop.
toSignal(): from an Observable to a signal
toSignal() subscribes to an Observable and exposes its latest value as a signal. The subscription is created on the call and destroyed automatically when the injection context disappears — no more manual unsubscribe or async pipe.
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { interval, map } from 'rxjs';
@Component({
selector: 'app-root',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<h1>Stopwatch</h1>
<p>Seconds elapsed: {{ secondes() }}</p>
`,
})
export class AppComponent {
readonly secondes = toSignal(
interval(1000).pipe(map((n) => n + 1)),
{ initialValue: 0 },
);
}
Try it live
rxjs-tosignal
Handling the initial value
An Observable may only emit after a delay. Before the first emission, the signal needs a value. Three strategies:
// 1. Provide an explicit initial value (recommended):
readonly data = toSignal(flux$, { initialValue: [] });
// 2. Accept undefined before the first emission:
readonly data = toSignal(flux$); // Signal<T | undefined>
// 3. Require an immediate synchronous emission (error otherwise):
readonly data = toSignal(flux$, { requireSync: true });
toObservable(): from a signal to an Observable
Conversely, toObservable() turns a signal into an Observable. Handy to reuse the power of RxJS operators (debounceTime, switchMap, combineLatest…) from a state held in a signal.
import { Component, signal, inject, ChangeDetectionStrategy } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { Observable, debounceTime, distinctUntilChanged, switchMap } from 'rxjs';
@Component({ /* … */ })
export class RechercheComponent {
private readonly http = inject(HttpClient);
readonly terme = signal('');
// The signal becomes a stream: we "debounce" the typing then trigger
// an HTTP search on each stabilized term.
private readonly resultats$ = toObservable(this.terme).pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap((q) => this.rechercher(q)),
);
// …then we return to the world of signals for the display.
readonly resultats = toSignal(this.resultats$, { initialValue: [] });
private rechercher(q: string): Observable<string[]> {
return this.http.get<string[]>(`/api/search?q=${q}`);
}
}
This round trip — signal → observable (for the operators) → signal (for the display) — is one of the most useful patterns of interop.
resource() and rxResource(): reactive async
Angular provides an API dedicated to loading asynchronous data driven by signals: resource() (and its RxJS variant rxResource()). A resource automatically re-runs its request when its source signals change, and exposes the state (value, status, error) as signals.
import { resource, signal } from '@angular/core';
const userId = signal(1);
const utilisateur = resource({
// When request changes, the loader is re-run.
request: () => ({ id: userId() }),
loader: async ({ request, abortSignal }) => {
const res = await fetch(`/api/users/${request.id}`, { signal: abortSignal });
return res.json();
},
});
// Reactive reading:
utilisateur.value(); // the data (or undefined during loading)
utilisateur.status(); // 'idle' | 'loading' | 'resolved' | 'error' …
utilisateur.isLoading();
resource()is recent (Angular 19, in developer preview). The API may still evolve; check your project's Angular version before using it in production. For a simple need,toSignal(this.http.get(...))remains perfectly valid.
When to use what?
| Situation | Recommended tool |
|---|---|
| Synchronous state that drives the UI (counter, selection, form) | signal / computed |
Keystrokes with debounce, throttling, stream merging |
RxJS (then toSignal for the display) |
| Single HTTP request displayed in the template | toSignal(http.get(...)) |
| Request that re-runs based on reactive parameters | resource() / rxResource() |
| Websocket, continuous events | RxJS |
Classic pitfall: creating toSignal/toObservable outside an injection context
Like effect(), these functions need an injection context to manage the subscription lifecycle. Declare them in a field initializer or in the constructor — not in ngOnInit nor in an event handler. If you really must do it elsewhere, pass an injector:
constructor(private injector: Injector) {}
ngOnInit() {
this.valeur = toSignal(this.flux$, { initialValue: 0, injector: this.injector });
}
Key points
toSignal(): Observable → signal, auto-managed subscription, handles the initial value.toObservable(): signal → Observable, to reuse RxJS operators.resource()/rxResource(): reactive asynchronous loading driven by signals (Angular 19, preview).- Create them in an injection context.
In chapter 8, we tie all of this to rendering: the new control flow (@if, @for), the OnPush strategy and the zoneless mode that signals make possible.