Interop between signals & RxJS

Key takeawaytoSignal() 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.

We use Microsoft Clarity to understand how the site is used and improve it. By continuing to browse, you accept it. You can disable it at any time.