rxResource()looks like a brand new API the first time you see it, but it is really justresource()wearing an RxJS costume. Same params, same idle status, same defaultValue, just swap the Promise for an Observable.

All examples use the Official Joke API through
HttpClient. No manual Promise wrapping this time,HttpClientalready hands back an Observable, and that plugs straight into thestreamoption.
resource() vs rxResource(): The One-Line Difference
resource() |
rxResource() |
|
|---|---|---|
| Import | @angular/core |
@angular/core/rxjs-interop |
| What you give it | An async loader returning a Promise<T> |
A stream function returning an Observable<T> |
| Needs RxJS? | No | Yes |
| Cancellation | You pass abortSignal into fetch yourself |
Angular calls .unsubscribe() on your Observable for you |
debugName support |
Yes | No, it is not part of its options type |
| Under the hood | The primitive itself | Literally calls resource() internally |
params, defaultValue, idle status |
Same | Same |
rxResource() is not a competing API, it is resource() with an RxJS-shaped hole for your stream function to fill. Every reactive params trick, every idle-status trick you already know from resource() carries over unchanged.
Setup: Joke Type + API Constants
Same shared types as the resource() note, this time consumed through HttpClient instead of raw fetch.
// jokes.ts
export interface Joke {
id: number;
type: string;
setup: string;
punchline: string;
}
// http-demo-service.ts
export const JOKES_API = {
JOKE_API_10_JOKES: 'https://official-joke-api.appspot.com/random_ten',
GET_JOKES_WITH_DYNAMIC_NUMBERS: (n: number) =>
`https://official-joke-api.appspot.com/jokes/random/${n}`,
};
Full Options Reference
rxResource<T, P>({
// ── BaseResourceOptions (identical to resource()) ────────────────────
// Reactive params: re-triggers the stream whenever these change.
// Return undefined to keep the resource idle.
params: () => ({ count: this.count() }),
// Returned while the resource is still loading or idle.
defaultValue: [] as T,
// Custom equality: skip re-renders when the data didn't meaningfully change.
equal: (a, b) => a.length === b.length && a.every((j, i) => j.id === b[i].id),
// SSR TransferState key: same role as in resource().
id: 'jokes-rx-resource',
// Normally omitted, useful when creating a resource outside an injection context.
injector: inject(Injector),
// ── RxJS-based stream ────────────────────────────
// Receives the same { params, abortSignal, previous } shape as resource()'s
// loader, but must return an Observable<T>, not a Promise<T>.
stream: ({ params, abortSignal }) => {
abortSignal.addEventListener('abort', () => console.log('cancelled'));
return this.#http.get<T>(`/api/jokes/${params.count}`);
},
});
One thing you will not find here: debugName. resource() gets it because its options type is wrapped in a separate ResourceOptions type that adds debugName on top. rxResource() skips that wrapper and extends BaseResourceOptions directly, so debugName never made it into the type. Try to pass it anyway and TypeScript stops you at compile time, not at runtime.
Example 1: The Pure Form
Same idea as resource()'s pure form: no params, no conditions, just a stream that fires once on creation.
In service we do:
import { inject, Service } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { rxResource } from '@angular/core/rxjs-interop';
import { Joke } from './jokes';
import { JOKES_API } from './http-demo-service';
@Service({ autoProvided: false })
export class RxResourceDemoService {
readonly #http = inject(HttpClient);
pureJokeDataResource = rxResource<Joke[], Record<never, never>>({
stream: () => this.#http.get<Joke[]>(JOKES_API.JOKE_API_10_JOKES),
});
}
HttpClient.get() already returns an Observable<Joke[]>, so there is nothing to wrap, nothing to .then(). The stream function just hands that Observable straight to Angular.
In component we do:
import { Component, inject } from '@angular/core';
import { RxResourceDemoService } from '../service/rx-resource-demo.service';
@Component({
selector: 'app-rx-jokes',
templateUrl: './rx-jokes.html',
})
export class RxJokesComponent {
#service = inject(RxResourceDemoService);
jokes = this.#service.pureJokeDataResource;
}
In HTML then we do:
@if (jokes.isLoading()) {
<p>Loading...</p>
} @else {
@for (joke of jokes.value(); track joke.id) {
<div>
<strong>{{ joke.setup }}</strong>
<p>{{ joke.punchline }}</p>
</div>
}
}
jokes.value() can be undefined here since no defaultValue was set, exactly like the resource() version. Use hasValue() or a defaultValue: [] before iterating.
Example 2: Reactive Params with Conditional Loading
This is the one worth slowing down on: reactive params and the idle-on-skip pattern, both in the same resource. Read a signal in params, and when there is nothing to fetch yet, return undefined instead of a guard inside the stream.
In service we do:
jokeDataResourceWithParams = (jokeCount: Signal<number | undefined>) => {
return rxResource<Joke[], { count: number } | undefined>({
params: () => (jokeCount() !== undefined ? { count: jokeCount()! } : undefined),
stream: ({ params }) =>
this.#http.get<Joke[]>(JOKES_API.GET_JOKES_WITH_DYNAMIC_NUMBERS(params.count)),
defaultValue: [],
});
};
Same rule as resource(): Angular tracks the signal read inside params, not inside stream. When jokeCount() is undefined, params returns undefined, the resource goes idle, and value() falls back to defaultValue automatically. No manual if (params.count === undefined) return [] needed anywhere in the stream function.
In component we do:
import { Component, inject, signal } from '@angular/core';
import { RxResourceDemoService } from '../service/rx-resource-demo.service';
@Component({
selector: 'app-rx-jokes-dynamic',
templateUrl: './rx-jokes-dynamic.html',
})
export class RxJokesDynamicComponent {
#service = inject(RxResourceDemoService);
jokesCount = signal<number | undefined>(undefined);
jokes = this.#service.jokeDataResourceWithParams(this.jokesCount);
}
In HTML then we do:
<input
#input
type="number"
placeholder="Enter a count to load jokes"
(input)="jokesCount.set(input.valueAsNumber)"
/>
@if (jokesCount() === undefined) {
<p>Enter a number above to fetch jokes.</p>
} @else if (jokes.isLoading()) {
<p>Loading...</p>
} @else if (jokes.error()) {
<p>Something went wrong: {{ jokes.error()?.message }}</p>
} @else {
@for (joke of jokes.value(); track joke.id) {
<div>
<strong>{{ joke.setup }}</strong>
<p>{{ joke.punchline }}</p>
</div>
} @empty {
<p>No jokes found for that count.</p>
}
}
Example 3: Cancellation Without AbortController
resource() needs you to thread an abortSignal into fetch yourself. rxResource() does not, because RxJS already knows how to tear a subscription down. All Angular has to do is call .unsubscribe().
In service we do:
import { inject, Service, Signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { rxResource } from '@angular/core/rxjs-interop';
import { concatMap, Subject, takeUntil, timer } from 'rxjs';
import { Joke } from './jokes';
import { JOKES_API } from './http-demo-service';
@Service({ autoProvided: false })
export class RxResourceDemoService {
readonly #http = inject(HttpClient);
// Angular unsubscribes the stream Observable on abort, reload, or destroy,
// no AbortController needed. This Subject is only for a manual cancel
// button that does not otherwise change params.
#manualCancel$ = new Subject<string>();
cancelJokeRequest(reason?: string): void {
this.#manualCancel$.next(reason ?? 'User cancelled');
}
jokeResourceWithAbortSignal = (jokeCount: Signal<number | undefined>) => {
return rxResource<Joke[], { count: number } | undefined>({
params: () => (jokeCount() !== undefined ? { count: jokeCount()! } : undefined),
defaultValue: [],
stream: ({ params, abortSignal }) => {
abortSignal.addEventListener(
'abort',
() => console.log(`Request aborted: ${abortSignal.reason}`),
{ once: true },
);
return timer(2000).pipe(
concatMap(() =>
this.#http.get<Joke[]>(JOKES_API.GET_JOKES_WITH_DYNAMIC_NUMBERS(params.count)),
),
takeUntil(this.#manualCancel$),
);
},
});
};
}
There is no AbortController anywhere in this example, and there does not need to be. When Angular fires abortSignal (params change before the previous request finishes, or the component gets destroyed), rxResource() calls .unsubscribe() on your Observable’s subscription for you. RxJS already knows how to tear itself down, so Angular just asks it to.
If your Observable completes without ever emitting a value, for example if takeUntil fires before the http.get() inside concatMap resolves, the resource does not quietly resolve to nothing. It resolves to an error: “Resource completed before producing a value.” Worth knowing before you spend ten minutes wondering why your resource is stuck in an error state with no failed network request to show for it.
In component we do:
import { Component, inject, signal } from '@angular/core';
import { RxResourceDemoService } from '../service/rx-resource-demo.service';
@Component({
selector: 'app-rx-jokes-cancel',
templateUrl: './rx-jokes-cancel.html',
})
export class RxJokesCancelComponent {
#service = inject(RxResourceDemoService);
count = signal<number | undefined>(5);
jokes = this.#service.jokeResourceWithAbortSignal(this.count);
cancel() {
this.#service.cancelJokeRequest('User clicked cancel');
}
}
In HTML then we do:
<input
#input
type="number"
placeholder="Enter count (2 sec delay)"
[value]="count()"
(input)="count.set(input.valueAsNumber)"
/>
@if (jokes.isLoading()) {
<p>Fetching... (2 sec delay)</p>
<button (click)="cancel()">Cancel request</button>
} @else if (jokes.error()) {
<p>Cancelled or failed: {{ jokes.error()?.message }}</p>
<button (click)="jokes.reload()">Retry</button>
} @else {
@for (joke of jokes.value() ?? []; track joke.id) {
<p><strong>{{ joke.setup }}</strong>: {{ joke.punchline }}</p>
}
}
Quick Comparison
| Pattern | Has params? |
Signal-reactive? | Skippable? | Cancellation |
|---|---|---|---|---|
| Pure form | No | No | No | Automatic on destroy |
| Reactive params + conditional | Yes | Yes | Yes (return undefined from params) |
Automatic on param change or destroy |
| Cancellation example | Yes | Yes | Yes | Automatic, plus a manual Subject for a cancel button |
Same ResourceRef API
Whatever rxResource() returns is still a ResourceRef<T>, the exact same interface resource() returns. value(), status(), error(), isLoading(), hasValue(), reload(), set(), update(), destroy(), all identical. If you already read the resource() note’s full API reference, there is nothing new to learn here, just a different way of filling in stream.
jokes.value() // T | undefined, same as resource()
jokes.isLoading() // boolean, same as resource()
jokes.hasValue() // boolean, same as resource()
jokes.error() // Error | undefined, same as resource()
Follow me for more
Source: Original article on HackMD. Local review copy retrieved September 10, 2026.