← Retour au blog
AngularRxJSSignalsMigrationReactive Programming

RxJS to Signals: Mastering toSignal and toObservable for Progressive Migration

Angular 16 introduced Signals as a fundamental reactive primitive, yet RxJS remains pervasive in existing codebases. The real question isn't "Signals or RxJS?" but rather "How do we blend them intelligently?" The toSignal and toObservable functions are your transition bridges. They enable gradual migration without a complete architectural rewrite. Understanding their costs, limitations, and specific use cases is essential for writing maintainable and performant code.

The Context: Why This Duality?

RxJS is built on declarative observation and chained operators. Signals are a more direct approach: fine-grained reactivity at the source, without observable overhead. Angular chose a pragmatic strategy: Signals become the base reactive layer (change detection, stores, etc.), but RxJS remains valuable for complex flows, timers, and HTTP requests. You're not forced to rewrite everything. Teams with deeply nested observables, sophisticated operators, or heavy RxJS dependencies benefit from a gradual migration. The idea is to leverage Signals for new business logic while keeping RxJS where it truly excels.

toSignal: Converting an Observable to a Signal

toSignal takes an observable and returns a signal that emits the last received value. It's the tool for integrating RxJS sources into your Signal logic. The signature is straightforward: toSignal(observable, options?). You can pass an initial value via options.initialValue, which prevents undefined during loading. Concrete example: you have an HTTP service returning an observable, and you want to use it within a computed or effect Signal.

import { toSignal } from '@angular/core';

import { HttpClient } from '@angular/common/http';

export class UserStore {

private http = inject(HttpClient);

private userObservable$ = this.http.get('/api/user');

// Convert to Signal

user = toSignal(this.userObservable$, { initialValue: null });

// Use in a computed

userDisplay = computed(() => {

const u = this.user();

return u ? ${u.name} (${u.role}) : 'Loading...';

});

}

Mind the subscription: toSignal automatically creates an internal subscription managed by Angular. You must not manually subscribe to the observable passed in. The signal always reflects the latest value, which works perfectly for simple data like user preferences, form data, or API responses. If you need to react to changes with complex side-effects, an effect is more appropriate than a computed.

toObservable: Converting a Signal to an Observable

This is the inverse: toObservable takes a signal and returns an observable that emits every time the signal changes. Useful when you have Signal-based reactive logic but need to pass it to a library or RxJS operator. For instance, integrating with ngxs, complex rxjs-operators, or a third-party API that only accepts observables.

import { toObservable } from '@angular/core';

import { map, debounceTime, distinctUntilChanged } from 'rxjs/operators';

export class SearchComponent {

query = signal('');

// Convert signal to observable to apply RxJS operators

results$ = toObservable(this.query).pipe(

debounceTime(300),

distinctUntilChanged(),

switchMap(q => this.searchService.find(q))

);

}

Here, the query signal is converted to an observable to leverage debounceTime and switchMap operators. Each signal mutation emits a new value in the observable, triggering the RxJS pipeline. It's elegant and readable: the source is a Signal, the transformation is RxJS, and you can consume the result via async pipe or toSignal again.

Realistic Use Cases and Architectural Choices

A filter component with real-time search benefits from a mixed approach. The search term is a signal (reactive, fast), but the HTTP request and debouncing flow through RxJS. You end up with an observable of results, which you reconvert to a signal to avoid the async pipe and facilitate computed properties. For a homemade NgRx-like store, you can store state in Signals and expose selectors as observables via toObservable, maintaining an Observable-first API for existing consumers while leveraging Signals internally.

Another pattern: dependencies between signals and observables. If a signal depends on an observable (toSignal), and an observable depends on a signal (toObservable), you create a coherent reactive chain. Be careful not to create infinite loops: a signal triggering an observable that updates the signal can cause excessive updates. Use distinctUntilChanged or explicit guards to prevent this.

Common Pitfalls

The first pitfall: forgetting that toSignal creates a subscription. If you call toSignal on a cold observable (like httpClient.get), the request launches when the signal is created, even if nobody consumes it. For a hot observable, that's fine. For lazy-loading, you need to think about lazy signals or manually trigger. Second pitfall: initialValue. If you don't pass initialValue, the signal starts undefined until the first observable emission. This can break your business logic if you assume a value is present. Always set a sensible default.

The third pitfall: performance. Converting an observable that emits rapidly to a signal can overload Angular's change detection. If you have an observable emitting 100 times per second (sensor, WebSocket), toSignal updates the signal 100 times per second, triggering change detection each time. Use RxJS operators like throttleTime or debounceTime before toSignal to limit emissions. Finally, circular dependencies between signals and observables are subtle but dangerous: a signal triggering an observable that updates the signal can create infinite loops. Always trace your data flow and use Angular DevTools to debug.

Progressive Migration Strategy

Start by identifying entry points: HTTP services, reactive forms, stores. For each HTTP service, create a version that exposes toSignal alongside the observable. This lets new components use Signals without impacting old ones. For forms, FormGroup.valueChanges remains an observable, but you can convert it via toSignal for Signal-based components. NgRx stores can expose both observables (via existing selectors) and signals (via toSignal-wrapped selectors).

A realistic approach: hybridize by business domain. If you have a user management module, migrate its components and store to Signals, exposing an observable-compatible API via toObservable. Other modules keep RxJS. Gradually, you move toward Signals. This strategy reduces risk, lets you validate the pattern on a small scope, and eases team learning.

Operational Conclusion

toSignal and toObservable are not complete migration tools, but pragmatic bridges. Use toSignal to integrate observables into your Signal logic, being careful to manage initialValue and subscription properly. Use toObservable to expose signals to code or libraries expecting an observable. Avoid unnecessary conversions: if you stay in Signal-land, stay there. If you stay in RxJS-land, stay there. Conversions carry a cost, however minor. Plan a gradual migration by domain, not a big-bang. Validate your reactive architecture with Angular DevTools and careful profiling of change detection updates. The goal is more maintainable, performant code—not dogmatic Signal adoption.

Développeur Angular & Mobile freelance — Strasbourg.

© 2026 Emilien Pons — Tous droits réservés.Conçu avec Angular, PrimeNG et ❤️