Skip to content

RxJS or the Resource API?#

Angular ships a signals-native answer for async data: resource, its HTTP wrapper httpResource, and rxResource, which lives in @angular/core/rxjs-interop, the same package as toSignal and takeUntilDestroyed. Interviewers now follow any hand-built HTTP pipeline with the obvious question: "why not just use httpResource?" This page is the answer, and the decision rule behind it.

At a glance

  • Resources win for the narrow, common case: re-fetch when a parameter changes, expose value(), isLoading(), error(), and status() as signals
  • RxJS wins the moment anything is stream-shaped: debouncing, retry with backoff, cancellation you control, combining event sources, multicasting, or the stream itself as the value
  • rxResource is the bridge: an Observable-returning loader with resource semantics for the consumer
  • Top gotcha: resources solve data fetching; they do not replace RxJS's event and time toolbox

What a resource gives you#

A resource is a reactive fetch bound to signal inputs. When the params computation changes, the loader re-runs and the previous request is dropped. One cancellation nuance up front: httpResource genuinely cancels the stale HTTP request, while the generic resource() only signals its loader's AbortSignal; the underlying work stops only if the loader honors that signal (for example by passing it to fetch).

import { Component, inject, input } from "@angular/core";
import { httpResource } from "@angular/common/http";

@Component({
  selector: "app-user-detail",
  template: `
    @if (user.isLoading()) {
      <p>Loading...</p>
    } @else if (user.error()) {
      <p>Could not load user.</p>
    } @else if (user.hasValue()) {
      <p>{{ user.value().name }}</p>
    }
  `,
})
export class UserDetailComponent {
  readonly userId = input.required<number>();

  // re-fetches whenever userId changes; previous request is aborted
  protected readonly user = httpResource<{ name: string }>(
    () => `/api/users/${this.userId()}`,
  );
}

Compare that with the equivalent stream on the HttpClient patterns page: toObservable + switchMap + a discriminated-union state + toSignal. The resource version is shorter, and the loading/error state comes built in (status() reports idle, loading, reloading, resolved, local, or error). It also has reload() for the refresh-trigger case that Caching Level 2 builds by hand with a Subject + switchMap, and it supports server-side rendering caching through a stable id.

For "fetch this when that changes, and show me loading and error state", the resource is enough. Reaching for RxJS there is no longer the strongest answer; knowing that is part of the interview.

Where RxJS stays the right tool#

Everything stream-shaped sits outside the resource model:

Need Why a resource cannot RxJS tool
Debounce user input before fetching params reacts to every change immediately debounceTime
Retry with backoff, gated by error type Loader failure goes straight to error() retry({ count, delay })
Queue or drop bursts (saves, submits) One loader per params change, latest wins, always concatMap / exhaustMap
Combine several live sources Resources track one params computation combineLatest, merge
Share one execution among many consumers Each resource owns its own loader shareReplay
Continuous streams (WebSockets, events) The loader resolves once per request Subjects, fromEvent, resource's stream option for simple cases

The interview framing: a resource replaces the last step of many pipelines (fetch + expose state), never the middle (time, ordering, combination, sharing). The site's typeahead pipeline survives intact: debounce, distinctUntilChanged, and cancellation semantics are stream concerns.

rxResource, the bridge#

When the loader is naturally an Observable (an existing service method, a stream with retry logic baked in) but the consumer wants resource semantics:

import { rxResource } from "@angular/core/rxjs-interop";

protected readonly user = rxResource({
  params: () => ({ id: this.userId() }),
  stream: ({ params }) =>
    this.api.getUser(params.id).pipe(retry({ count: 2, delay: 1000 })),
});

The pipeline keeps its RxJS powers; the template gets value(), isLoading(), and error(). This is often the honest best-of-both answer.

Interview Q&A#

Why not just use httpResource for the typeahead search?

Because typeahead is mostly not a fetching problem. The pipeline needs debouncing (a resource's params reacts to every keystroke instantly) and deliberate cancellation of the settled-but-stale request. httpResource does cancel its stale request on params changes (a custom resource() loader must honor the AbortSignal itself), but no resource can debounce, dedupe, or throttle its inputs. The strong answer: debounce and shape the stream with RxJS, then hand the final fetch to rxResource, or keep switchMap + toSignal.

When would you actively choose a resource over a stream?

Parameter-driven reads with standard loading/error UI: detail pages keyed by a route param or input signal, dashboards re-fetching on filter changes. httpResource's cancel-on-change covers the stale-response problem (custom loaders get an AbortSignal to honor), reload() covers refresh, and there is less code to review.

Does the Resource API make shareReplay-style caching obsolete?

No. A resource is owned by its consumer; two components creating resources for the same URL fetch twice. Sharing one execution across consumers is still multicasting: a service exposing a shareReplay stream, or a service-owned resource that everyone reads.