In Angular, how does rxResource() differ from resource() when the order-history service you call already returns an Observable?
answer
- rxjs-interop package
- stream option, not loader
- every emission updates value
- unsubscribed on new params
- completing empty is an error
basics
~20 srxResource() from @angular/core/rxjs-interop takes a stream function returning an Observable instead of a promise loader; it subscribes per params value, updates value() on every emission and unsubscribes when params change, while exposing the same Resource API.
solid answer
~40 s`rxResource()` lives in `@angular/core/rxjs-interop` and is `resource()` with an Observable loader. Instead of `loader`, you pass `stream: ({params, abortSignal, previous}) => Observable<T>`. Angular subscribes for each params value; the first emission resolves the resource and later emissions keep updating `value()`, so a live feed works too. When params change, `set()` is called or the resource is destroyed, Angular unsubscribes from the old Observable — a switch to the newest request — and an Observable `error` puts the resource in `'error'`. Everything else is the same API: `status()`, `hasValue()`, `reload()`, `set()`, `defaultValue`. One trap: an Observable that completes without emitting, as with `catchError(() => EMPTY)`, leaves the resource in an error state with `NG0991`.
code
ts · 33 linesimport { Component, inject, input } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { rxResource } from '@angular/core/rxjs-interop';
import { catchError, of } from 'rxjs';
interface Order {
id: string;
total: number;
}
@Component({
selector: 'app-customer-orders',
template: `
@if (orders.hasValue()) {
<p>{{ orders.value().length }} orders</p>
} @else if (orders.isLoading()) {
<p>Loading orders...</p>
}
`,
})
export class CustomerOrders {
private readonly http = inject(HttpClient);
readonly customerId = input<string | undefined>();
readonly orders = rxResource({
params: () => this.customerId(),
stream: ({ params: id }) =>
this.http
.get<Order[]>(`/api/customers/${id}/orders`)
// Recover with a value, never with EMPTY.
.pipe(catchError(() => of([] as Order[]))),
});
}go deeper
Recall that rxResource comes from @angular/core/rxjs-interop and takes a stream function returning an Observable.
Explain how emissions map to value(), that new params unsubscribe the old stream, and why completing without a value is an NG0991 error.
Choose between rxResource, httpResource and resource for an existing codebase, and review stream pipelines for error handling that completes empty.
Set the boundary between RxJS services and signal-based components so teams know which layer owns streams and which owns resources.
## Why a second function exists Plenty of Angular code returns **Observables**: `HttpClient` methods, services built on `BehaviorSubject`, WebSocket wrappers. `resource()` expects a loader that returns a promise, and converting each Observable to a promise by hand throws away its streaming and cancellation. `rxResource()`, exported from `@angular/core/rxjs-interop`, keeps the `resource()` model — reactive `params`, status signals, reload and local writes — but accepts an Observable as the source. This answer assumes Angular 22.2, where `rxResource()` is stable public API. ## The shape of the API - **Import:** `import { rxResource } from '@angular/core/rxjs-interop';` - **Options:** the same `params`, `defaultValue`, `equal`, `injector` and `id` as `resource()`, plus a **`stream`** function. - **`stream`** receives the same `ResourceLoaderParams` object as a loader — `params`, `abortSignal`, `previous` — and returns an `Observable<T>`. - **Return value:** a `ResourceRef`, so `value()`, `status()`, `error()`, `isLoading()`, `hasValue()`, `reload()`, `set()`, `update()` and `destroy()` are all there. ## How Angular drives the Observable 1. `params` produces a value (an `undefined` value keeps the resource `'idle'`, as with `resource()`). 2. Angular calls `stream` and **subscribes** to the returned Observable. 3. The first `next` resolves the resource: `status()` becomes `'resolved'` and `value()` holds the emission. 4. Each later `next` **replaces** `value()`. The resource stays `'resolved'`, so a long-lived stream of order updates keeps the view current. 5. An `error` notification moves the resource to `'error'`, and `error()` returns it. 6. When `params` changes, `set()` is called or the resource is destroyed, Angular fires the load's abort signal, and `rxResource` **unsubscribes** from the old Observable before subscribing to the new one. Step 6 is the behaviour RxJS users know from `switchMap`: only the newest request's stream is alive. For an `HttpClient` call, unsubscribing cancels the underlying HTTP request, so you get real cancellation without touching `abortSignal`. ## Side by side | | `resource()` with `loader` | `rxResource()` with `stream` | | --- | --- | --- | | Package | `@angular/core` | `@angular/core/rxjs-interop` | | Source type | Promise | Observable | | Values per request | One | One or many | | Cancellation | You pass `abortSignal` on | Unsubscribe happens for you | | Empty completion | Not applicable | Error `NG0991` | ## The empty-completion trap A resource must end up with *something*: a value or an error. If the Observable **completes without emitting**, Angular has neither and records an error with code `NG0991` ("Resource completed before producing a value"). The error surfaces the next time something reads `value()` — often a template far from the cause. The usual culprit swallows errors: ```ts stream: ({ params: id }) => this.api.orders(id).pipe(catchError(() => EMPTY)), // completes empty: NG0991 ``` Two fixes, depending on intent: - **Let the error through** — drop the `catchError`, and render the `'error'` state with `error()`. - **Recover with a value** — `catchError(() => of([] as Order[]))` resolves the resource with an empty list. Combining sources with operators such as `merge` or `race`, where every source can complete before emitting, causes the same error. ## When not to reach for it - For a plain HTTP read with no existing Observable service, `httpResource()` in `@angular/common/http` is the dedicated tool. - To turn an Observable into a signal without params-driven requests, `toSignal()` fits better. - For promise-based APIs, plain `resource()` with a `loader` is simpler.
- What status does an rxResource report while a long-lived Observable keeps emitting?`'resolved'`. The first emission resolves the resource, and each later emission replaces `value()` without passing through `'loading'` again. The status only changes when params change, `reload()` is called, the stream errors, or the value is set locally.
- How does reload() behave on an rxResource?It calls `stream` again for the same params and subscribes to the new Observable, reporting `'reloading'` meanwhile and keeping the old value visible. Like `resource()`, it returns `false` and does nothing if a load is already in progress.
rxResource is like a radio tuned by a dial: turning the dial (new params) drops the old station before tuning the new one, and a station can keep broadcasting updates, but a station that goes silent without saying anything counts as a fault.
saying these in an interview costs you the question
- rxResource takes a loader function that returns an Observable.
- rxResource only uses the first emission and ignores later values.
- You must add takeUntilDestroyed to the stream to avoid leaks.
- catchError(() => EMPTY) is a safe way to hide a failed request.
- Old subscriptions stay open until the component is destroyed.