Signals & RxJS Interop#
Angular now has two reactive systems: signals for synchronous state and RxJS for asynchronous streams and event orchestration. @angular/core/rxjs-interop bridges them with toSignal and toObservable, and the modern interview question is no longer "what is an Observable" but "when do you use which".
At a glance
toSignal(obs$, opts): subscribes immediately, exposes the latest value as a signal, unsubscribes when the injection context is destroyedtoObservable(signal): emits the signal's value on change, as a stream, using an effect under the hood- Rule of thumb: state that is something → signal; things that happen over time (events, requests, retries, debouncing) → RxJS
- Top gotcha:
toObservablecoalesces synchronous updates; rapid intermediate signal values are not individually emitted
The Division of Labor#
| Concern | Reach for |
|---|---|
| Current value templates render | Signal |
| Derived synchronous state | computed() |
| HTTP requests, cancellation, retries | RxJS |
| Debouncing, throttling, timing | RxJS |
| Combining event streams | RxJS |
| Bridging the two | toSignal / toObservable |
The productive pattern in modern components: RxJS at the edges (async orchestration), signals at the surface (template state).
toSignal: Stream In, Signal Out#
import { Component, inject } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { toSignal } from "@angular/core/rxjs-interop";
@Component({
selector: "app-profile",
template: `
@if (user(); as u) {
<h2>{{ u.name }}</h2>
} @else {
<p>Loading...</p>
}
`,
})
export class ProfileComponent {
private readonly http = inject(HttpClient);
// subscribes NOW, cleans up on destroy, exposes the latest value
protected readonly user = toSignal(
this.http.get<{ name: string }>("/api/me"),
);
}
The value options matter and interviewers probe them:
- No option: the signal's type includes
undefineduntil the first emission. { initialValue: x }: noundefined;xuntil the stream emits.{ requireSync: true }: the stream must emit synchronously at subscribe (aBehaviorSubject, or anything piped throughstartWith); otherwise it throws.
Two behaviors to know: toSignal subscribes eagerly (not on first read), and if the source errors, reading the signal throws that error. It needs an injection context (or an explicit injector option) to schedule its cleanup.
toObservable: Signal In, Stream Out#
Use it when signal-shaped state must drive stream-shaped work, the classic case being a signal input feeding a debounced search:
import { Component, inject, input } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { toObservable, toSignal } from "@angular/core/rxjs-interop";
import {
catchError,
debounceTime,
distinctUntilChanged,
of,
switchMap,
} from "rxjs";
@Component({
selector: "app-results",
template: `
<ul>
@for (r of results(); track r) {
<li>{{ r }}</li>
}
</ul>
`,
})
export class ResultsComponent {
private readonly http = inject(HttpClient);
readonly query = input.required<string>(); // signal input from the parent
// signal -> stream -> debounced, cancelled HTTP -> signal
protected readonly results = toSignal(
toObservable(this.query).pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap((q) =>
this.http.get<string[]>("/api/search", { params: { q } }).pipe(
// catch inside switchMap: an error must not kill the query stream
catchError(() => of<string[]>([])),
),
),
),
{ initialValue: [] },
);
}
Coalescing caveat: toObservable observes the signal with an effect, so several synchronous set() calls produce one emission with the final value. It is a state bridge, not an event log; if every event matters, keep a Subject as the source of truth.
"Why not debounced()?" Angular ships a (currently experimental) debounced() helper that debounces a signal's value, and Signal Forms has its own field-level debouncing. For the plain search case, a debounced signal driving httpResource is a legitimate composition: the debounce settles the parameter and httpResource cancels the stale request on each change. The stream pipeline earns its place when the requirement grows past that pairing: skipping unchanged values (distinctUntilChanged), retry with backoff, combining several inputs, cancelling non-HTTP work (any inner Observable, not just requests), or keeping the entire policy in one reviewable pipeline. Naming that boundary, rather than defending RxJS reflexively, is the strong interview answer. Full decision guide: RxJS or the Resource API?
The Rest of the Interop Package#
Three more exports round out @angular/core/rxjs-interop, and all are fair interview game precisely because they live in the RxJS interop layer:
outputFromObservable(stream$): exposes a stream as a component output, so(saved)="..."event bindings work against an RxJS pipeline. The natural "how do you emit a debounced output?" answer: build the pipeline, wrap it once. Its inverse,outputToObservable(componentRef.instance.saved), turns any output back into a stream for composition.pendingUntilEvent(): an operator that keeps a server-rendered app "unstable" until the observable emits, completes, errors, or is unsubscribed, so serialization waits for the data. It is the RxJS-shaped half of the SSR-stability story and a strong senior mention.rxResource(): the Observable-loader variant of the Resource API, covered on RxJS or the Resource API?
Common Mistakes#
Calling toSignal outside an injection context. In a method or callback it throws (or, with a manually passed injector, silently creates long-lived subscriptions per call). Create interop signals in field initializers or the constructor.
Expecting toObservable to emit every intermediate value. set(1); set(2); set(3) synchronously yields a single emission of 3. Event semantics need a Subject, not a signal bridge.
Rebuilding RxJS features on signals. Debounce, retry, cancellation, and race-condition control are stream problems. A signal + effect + setTimeout re-implementation of debounceTime is an anti-pattern interviewers increasingly test for.
Interview Q&A#
When would you still choose RxJS in a signals-first codebase?
Whenever time or events are involved: HTTP orchestration with cancellation (switchMap), debouncing and throttling, retries and backoff, combining event sources, WebSocket streams. Signals hold the resulting state; RxJS produces it.
What are the three initial-value strategies for toSignal?
Default (type includes undefined until first emission), initialValue (explicit placeholder, no undefined in the type), and requireSync: true (source must emit synchronously or it throws; pairs with BehaviorSubject or startWith). Choosing deliberately shows real-world use.
What happens if the Observable behind toSignal errors?
The error is rethrown when the signal is read, typically during change detection. Production pipelines should catchError into a safe value before the bridge, exactly like the async pipe story.
Related#
- RxJS or the Resource API?, the boundary this page's pipelines sit on
- Async Pipe, the template-side alternative to toSignal
- BehaviorSubject, the RxJS state-holder signals often replace
- switchMap and debounceTime, the stream tools worth bridging for