RxJS and Signals: Better Together
Bridge RxJS observables and Angular signals with toSignal() and toObservable() for seamless reactive architectures across the legacy/modern boundary.
title: RxJS and Signals: Better Together slug: rxjs-signals-interop summary: Learn how to bridge RxJS observables and Angular signals using toSignal() and toObservable() for seamless reactive architectures. date: 2024-04-10 author: Alex Rivera category: angular tags: [rxjs, signals, angular] status: published featured: false#
RxJS and Signals: Better Together#
Angular Signals and RxJS are not competitors — they are complementary tools for different reactive patterns. RxJS excels at handling asynchronous streams, events over time, and complex orchestration. Signals excel at synchronous, fine-grained state management with minimal boilerplate. Angular 18 provides first-class interop between the two through @angular/core/rxjs-interop.
When to Use Which#
Use Signals for:
- Component and service state that changes synchronously
- Derived values that update automatically when their dependencies change
- Simple UI state like counters, toggles, and form values
Use RxJS for:
- HTTP requests and other asynchronous operations
- Event streams from user input, WebSockets, or timers
- Complex coordination like debounce, throttle, switchMap, and combineLatest
Converting Observables to Signals#
The toSignal() utility bridges RxJS Observables into the signal world. It must be called in an injection context — typically as a field initializer in a component or service.
import { toSignal } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { inject } from '@angular/core';
@Component({ ... })
export class ArticleListComponent {
private readonly http = inject(HttpClient);
readonly articles = toSignal(
this.http.get<Article[]>('/api/articles'),
{ initialValue: [] as Article[] }
);
}
The initialValue option is required when the Observable may not emit synchronously. Without it, the signal would be undefined until the first emission, which can cause template errors.
By default, toSignal() automatically unsubscribes from the Observable when the component or service is destroyed. You do not need to manage subscriptions manually.
Converting Signals to Observables#
The toObservable() utility goes the other direction, turning a signal into an Observable that emits whenever the signal's value changes.
import { toObservable } from '@angular/core/rxjs-interop';
import { debounceTime, switchMap } from 'rxjs/operators';
@Component({ ... })
export class SearchComponent {
readonly query = signal('');
private readonly query$ = toObservable(this.query);
readonly results = toSignal(
this.query$.pipe(
debounceTime(300),
switchMap(q => this.searchService.search(q)),
),
{ initialValue: [] as SearchResult[] }
);
}
This pattern is powerful because it lets you use the full RxJS operator toolkit on signal-derived streams. The debounceTime operator prevents excessive search requests while the user is typing, and switchMap cancels in-flight requests when a new query arrives.
Cleaning Up with takeUntilDestroyed#
When you subscribe to Observables manually — rather than through toSignal() — use takeUntilDestroyed() to ensure automatic cleanup.
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { interval } from 'rxjs';
@Component({ ... })
export class TimerComponent {
private readonly destroyRef = inject(DestroyRef);
readonly elapsed = signal(0);
constructor() {
interval(1000)
.pipe(takeUntilDestroyed())
.subscribe(() => this.elapsed.update(n => n + 1));
}
}
takeUntilDestroyed() uses the component's DestroyRef internally, so it does not require the destroyRef variable unless you are passing it explicitly to another function.
Migration Patterns#
If you have an existing RxJS-heavy service, you do not need to rewrite it. Expose signals at the component layer and keep Observables in services where async operations live.
@Injectable({ providedIn: 'root' })
export class ArticleService {
private readonly http = inject(HttpClient);
getArticles(): Observable<Article[]> {
return this.http.get<Article[]>('/api/articles').pipe(
shareReplay({ bufferSize: 1, refCount: true }),
);
}
}
@Component({ ... })
export class ArticleListComponent {
private readonly articleService = inject(ArticleService);
readonly articles = toSignal(this.articleService.getArticles(), { initialValue: [] });
}
This layered approach keeps your services framework-agnostic while letting components enjoy the simplicity of signals in templates.
Common Pitfalls#
Calling toSignal outside injection context: toSignal() requires an injection context because it needs a DestroyRef to manage subscriptions. Calling it inside a method or callback will throw an error.
Forgetting initialValue: If the Observable emits asynchronously, the signal will be undefined until the first emission. Always provide an initialValue for UI-facing signals.
Overusing toObservable: Converting a signal to an Observable, applying one operator, and converting back to a signal is verbose. For simple transformations, prefer computed() over toObservable() + map + toSignal.
RxJS and signals together give you the best of both worlds: the power of reactive streams for async work and the simplicity of signals for stateful UI.