skip to content

In Angular, how does rxResource() differ from resource() when the order-history service you call already returns an Observable?

level: middleimportance: should knowfreq 42%

answer

  1. rxjs-interop package
  2. stream option, not loader
  3. every emission updates value
  4. unsubscribed on new params
  5. completing empty is an error

basics

~20 s

rxResource() 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 lines
ts
import { 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

for a junior

Recall that rxResource comes from @angular/core/rxjs-interop and takes a stream function returning an Observable.

for a middle

Explain how emissions map to value(), that new params unsubscribe the old stream, and why completing without a value is an NG0991 error.

for a senior

Choose between rxResource, httpResource and resource for an existing codebase, and review stream pipelines for error handling that completes empty.

for a principal

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.