Side-by-side RxJS to Angular signals reference: toSignal, httpResource, rxResource, resource, linkedSignal and takeUntilDestroyed, type-checked against Angular 22.
Showing 40 of 40 mappings
rxjs / rxjs/operators
@angular/core + rxjs-interopSetup
RxJS
import { BehaviorSubject, combineLatest, Subject } from 'rxjs';
import { map, switchMap, takeUntil } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';
Signals
import { signal, computed, effect, linkedSignal, resource } from '@angular/core';
import { toSignal, toObservable, rxResource, takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { httpResource } from '@angular/common/http';
Signal primitives and resource() come from @angular/core. Everything that touches an Observable lives in @angular/core/rxjs-interop, and httpResource() sits next to HttpClient.
private userService = inject(UserService);
user = toSignal(this.userService.user$);
<!-- template -->
@if (user(); as user) {
<p>{{ user.name }}</p>
}
toSignal() subscribes straight away and unsubscribes when the component is destroyed, so it must run in an injection context (a field initializer or the constructor). Without an initial value the type is Signal<User | undefined>.
Use requireSync for sources that emit synchronously on subscribe, such as a BehaviorSubject or anything piped through startWith(). The type has no undefined, and toSignal throws at runtime if the source does not emit right away.
Calling toSignal() outside an injection context throws NG0203. Pass an injector if you must create it later, but when the Observable depends on an input, rxResource() (see rxResource & resource) is usually the better fit because it re-runs when the input changes.
By default toSignal() unsubscribes when its injection context is destroyed. manualCleanup keeps the subscription until the Observable completes on its own, which suits one-shot sources like an HTTP GET.
A writable signal replaces the BehaviorSubject, and asReadonly() replaces asObservable(). Readers call cartStore.items() with no subscription, and the current value is always available.
There is no list of sources to keep in sync: computed() tracks every signal it reads. No glitches either, as a change to both signals produces one recomputation.
user$ = new BehaviorSubject<User>(guest);
address$ = this.user$.pipe(
map((user) => user.address),
distinctUntilChanged((a, b) => a.city === b.city),
);
Signals
user = signal<User>(guest);
address = computed(() => this.user().address, {
equal: (a, b) => a.city === b.city,
});
Signals already skip values that are Object.is-equal, so a bare distinctUntilChanged() needs no replacement. Pass equal for a custom comparison; signal(), toSignal(), linkedSignal() and the resource APIs accept it too.
Signals hold their latest value, so there is nothing to replay. A resource in a root service loads once, and every component that reads settings.value() shares that result.
Use effect() to push signal values into non-reactive APIs such as storage, logging or a third-party widget. Don’t use it to copy one signal into another; that is what computed() and linkedSignal() are for.
id = input.required<string>(); // route param via withComponentInputBinding()
user = httpResource<User>(() => `/api/users/${this.id()}`);
httpResource() sends a GET whenever a signal read in the function changes and cancels the request still in flight, like switchMap. It goes through HttpClient, so interceptors and HttpTestingController still apply.
Return a request object for anything beyond a plain URL: method, body, params, headers, withCredentials, timeout, transferCache and the fetch options (cache, priority, keepalive and so on).
Returning undefined sends no request and puts the resource in the idle status, where value() is the defaultValue (undefined if you set none). Unlike filter(), the previous results are not kept.
user = httpResource(() => '/api/me', { parse: UserSchema.parse });
parse receives the raw response and its return type becomes the resource type, so a Zod (or similar) schema gives runtime validation and a typed value() in one step. A thrown parse error puts the resource in the error status.
Keep POST, PUT and DELETE on HttpClient. Resources are for reads: they cancel in-flight requests when their params change, which could abort a write. Reload the affected resource once the write succeeds.
rxResource() keeps your existing Observable-returning services. The options are params and stream (request and loader in the v19 preview). The Observable must emit or error before completing, or Angular throws NG0991.
Every emission updates price.value(), and the socket closes when the component is destroyed. Without params the stream starts once; add params to reconnect when they change.
id = input.required<string>();
user = httpResource<User>(() => `/api/users/${this.id()}`);
company = httpResource<Company>(({ chain }) => {
const companyId = chain(this.user)?.companyId;
return companyId ? `/api/companies/${companyId}` : undefined;
});
chain() returns the other resource’s value once it has one. Until then, company copies user’s loading or error status instead of sitting idle, so one spinner or error message covers both requests.
A linkedSignal is writable like signal(), but it resets to the computation whenever options change. That replaces the effect or subscription people use to "reset on change".
The long form passes the previous source and value to the computation, so a selection survives a refresh when the item still exists. Give the generic types explicitly when you use previous.
toObservable() emits from an effect, so it is asynchronous: several synchronous set() calls produce one emission with the last value. Use it when you still need RxJS operators.
debounced() returns a resource that settles 300 ms after the last change. It is experimental in Angular 22, so production code may prefer toObservable() with debounceTime(), shown in the toObservable() mapping.
The output subscribes to the Observable and forwards every value to parent listeners, and unsubscribes when the component is destroyed. For outputs you emit by hand, use output() instead.
An output() is not an Observable, so it has no pipe(). Wrap it with outputToObservable() to use RxJS operators; the Observable completes when the component is destroyed.
Most Angular components use RxJS for two jobs: holding state that the template shows, and loading data. Angular 22 covers both with signals. signal(), computed() and linkedSignal() replace Subjects and operator chains for state, and httpResource(), rxResource() and resource(), stable since Angular 22, replace the switchMap plus loading-flag pattern for data. This reference pairs each RxJS pattern with its signal equivalent, grouped into 10 topics. The snippets are class members and assume private http = inject(HttpClient); the part after <!-- template --> goes in the component template.
Taken from the stability tags in the @angular/core and @angular/common 22.2 type definitions.
API
Import from
Status
signal() / computed()
@angular/core
Stable (since v17)
effect()
@angular/core
Stable (since v20)
linkedSignal()
@angular/core
Stable (since v20)
toSignal() / toObservable()
@angular/core/rxjs-interop
Stable (since v20)
takeUntilDestroyed()
@angular/core/rxjs-interop
Stable (since v19)
outputFromObservable() / outputToObservable()
@angular/core/rxjs-interop
Stable (since v19)
resource()
@angular/core
Stable (since v22)
rxResource()
@angular/core/rxjs-interop
Stable (since v22)
httpResource()
@angular/common/http
Stable (since v22)
debounced()
@angular/core
Experimental (since v22)
resourceFromSnapshots()
@angular/core
Experimental (since v22)
A Safe Migration Order
Upgrade to Angular 22 so the resource APIs are stable; the 21 → 22 upgrade plan lists every step.
Swap the async pipe for toSignal() at the edges, leaving services that return Observables as they are.
Turn BehaviorSubject stores into signal() and their derived streams into computed().
Replace switchMap data loading with httpResource(), or rxResource() where a service already returns an Observable, and delete the loading and error flags.
Keep RxJS where it earns its place: mutations through HttpClient, WebSockets, and timing operators via toObservable().
Yes. They were experimental from Angular 19 to 21 and are stable from Angular 22. resource() comes from @angular/core, rxResource() from @angular/core/rxjs-interop and httpResource() from @angular/common/http.
Do I have to remove RxJS?
No. RxJS is still a dependency of Angular and HttpClient still returns Observables. Signals are for state you read in templates and computed values; RxJS remains a good fit for event streams with timing operators. toSignal() and toObservable() connect the two.
What is the difference between httpResource and rxResource?
httpResource() builds the HTTP request for you from a URL or request object and exposes headers, statusCode and progress. rxResource() wraps any Observable you already have, such as a service method that returns this.http.get(), a WebSocket or a store selector.
Should I use httpResource for POST or PUT requests?
No. Resources are for reading data. They cancel the in-flight request when their params change, which could abort a write. Keep mutations on HttpClient and call reload() or update() on the resource afterwards.
When should I use linkedSignal instead of computed?
Use computed() when the value is fully derived and read-only. Use linkedSignal() when the user can also change it, such as a selected item that should reset (or be kept) when the list of options changes.
Why does toSignal throw NG0203?
toSignal(), toObservable(), takeUntilDestroyed() without an argument, and the resource functions need an injection context. Create them in a field initializer or the constructor, or pass an injector option.
Sources & Official Resources
Every signal snippet follows the official Angular guides and was type-checked against Angular 22.2. Last reviewed .