Signals

Angular rxResource Guide: Async Data with Signals (2026)

How to use Angular rxResource for reactive async data loading: signal-driven requests, error handling, SSR, and when to use rxResource vs resource.

7 min read
Angular rxResource Guide: Async Data with Signals (2026)
Share: X · LinkedIn

Angular’s rxResource is an RxJS-aware variant of the resource() primitive for loading async data with signals. It lets you model remote calls as reactive resources that automatically re-execute when their input signals change, while giving you the full power of RxJS operators for the actual request. If you have been mixing effect(), HttpClient, and manual signal updates to load data, rxResource is designed to replace that pattern with something declarative and SSR-safe.

What rxResource is for

rxResource represents an async data dependency whose request parameters come from signals and whose loader returns an Observable. Angular tracks the input signals, re-runs the loader when they change, and exposes the result as a signal-based resource.

The core value proposition:

  • Signal-driven: request inputs come from signals; the resource reacts automatically
  • RxJS-native loader: the loader returns an Observable — use any operators you need
  • Declarative states: status, value, error, isLoading are all signals
  • Lifecycle aware: cleanup, cancellation, and SSR are handled by the framework

In short, rxResource gives you the ergonomics of signals for the consumer side and the expressive power of RxJS for the producer side.

rxResource vs resource vs HttpClient + effect

Angular offers several ways to load async data today. Choosing between them comes down to whether your loader is Promise-based or Observable-based, and how much RxJS orchestration you need.

ApproachLoader typeBest for
resource()Promise / async functionSimple fetch calls, no operators needed
rxResource()ObservableHttpClient requests, RxJS operators, cancellation
HttpClient + effect()ObservableFine-grained manual control, legacy patterns
toSignal(httpClient.get(...))ObservableOne-shot reads with no request parameters

Rule of thumb: if your data source depends on one or more signals and you use HttpClient, reach for rxResource first.

Basic usage

A typical rxResource wraps an HTTP call and reacts to a signal-based request parameter.

import { Component, inject, signal } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';

@Component({
  standalone: true,
  template: `
    @if (user.isLoading()) {
      <p>Loading user…</p>
    } @else if (user.error()) {
      <p>Failed to load user.</p>
    } @else if (user.value(); as u) {
      <h2>{{ u.name }}</h2>
    }
  `
})
export class UserPageComponent {
  private readonly http = inject(HttpClient);
  readonly userId = signal(1);

  readonly user = rxResource({
    request: () => ({ id: this.userId() }),
    loader: ({ request }) =>
      this.http.get<User>(`/api/users/${request.id}`)
  });
}

When userId changes, rxResource cancels the in-flight request, re-invokes the loader with the new input, and updates isLoading, value, and error accordingly.

The resource state model

Every rxResource exposes a predictable set of signal-based readers:

  • value() — the current resolved value, or undefined
  • error() — the last error, or undefined
  • isLoading()true while a loader is in flight
  • status() — one of 'idle' | 'loading' | 'resolved' | 'error' | 'reloading'
  • hasValue()true when value() is set (useful for discriminating between “resolved with undefined” and “never resolved”)

These read like any other signal, so they integrate directly with templates, computed(), and effect().

Reacting to multiple signals

Because the request function reads signals, it composes naturally. The loader re-runs whenever any signal read inside request changes.

readonly searchTerm = signal('');
readonly page = signal(1);

readonly results = rxResource({
  request: () => ({
    q: this.searchTerm(),
    page: this.page()
  }),
  loader: ({ request }) =>
    this.http.get<SearchResults>('/api/search', {
      params: { q: request.q, page: request.page }
    })
});

Add debounceTime or distinctUntilChanged inside the loader pipeline if you need to protect the backend from rapid updates.

Cancellation and switchMap semantics

rxResource cancels the previous Observable when a new request fires. This behaves like switchMap — older in-flight requests are discarded, and only the most recent result reaches the resource. You get cancellation for free, without manually wiring takeUntil or subscriptions.

readonly posts = rxResource({
  request: () => ({ userId: this.userId() }),
  loader: ({ request, abortSignal }) =>
    this.http.get<Post[]>(`/api/posts?user=${request.userId}`, {
      // abortSignal lets the HTTP layer cancel the underlying XHR
      context: withAbortSignal(abortSignal)
    })
});

The abortSignal parameter ties the RxJS subscription lifecycle to the browser AbortController, which means cancelling the Observable also cancels the network request.

Reloading on demand

Call .reload() to force a fresh load without changing input signals. This is useful for pull-to-refresh, retry buttons, or post-mutation refresh.

readonly orders = rxResource({
  request: () => ({ userId: this.userId() }),
  loader: ({ request }) =>
    this.http.get<Order[]>(`/api/users/${request.userId}/orders`)
});

refresh(): void {
  this.orders.reload();
}

During a reload, status() returns 'reloading' and isLoading() stays true, while the previous value() remains available — good for keeping the UI stable.

Error handling patterns

Errors from the Observable propagate to error(). The resource does not re-throw; it captures the error so the template can decide how to render it.

readonly profile = rxResource({
  request: () => ({ id: this.userId() }),
  loader: ({ request }) =>
    this.http.get<Profile>(`/api/profiles/${request.id}`).pipe(
      catchError(err => {
        logger.error('profile load failed', err);
        return throwError(() => err);
      })
    )
});

For graceful degradation, map errors to a fallback value inside the loader and keep error() empty.

Computed values from a resource

Because value() is a signal, you can compose it with computed() for derived state.

readonly user = rxResource({ /* ... */ });

readonly initials = computed(() => {
  const u = this.user.value();
  if (!u) return '';
  return u.name.split(' ').map(p => p[0]).join('').toUpperCase();
});

This is the pattern that replaces the old “fetch in a service, store in BehaviorSubject, map with combineLatest” dance for synchronous derivations.

SSR considerations

rxResource is SSR-aware: during server-side rendering, Angular waits for the resource to resolve before serializing the response. This means article content, profile data, or any server-rendered view can rely on resource values being present in the initial HTML.

Key rules for SSR:

  • Prefer rxResource over manual HTTP calls inside ngOnInit when the data needs to be in the server response
  • Enable client hydration so the resource state transfers without a redundant client fetch
  • Avoid long-running loaders on SSR paths — they delay TTFB

For the broader SSR rendering strategy, see Angular SSR SEO Playbook.

Testing rxResource

Resources integrate cleanly with Angular’s testing APIs. The usual approach is to:

  1. Provide a mock HttpClient or testing backend
  2. Read the resource’s signal-based state in assertions
  3. Use TestBed.tick() or flush() to drain async work
it('loads user when userId changes', async () => {
  const fixture = TestBed.createComponent(UserPageComponent);
  const component = fixture.componentInstance;

  component.userId.set(42);
  await fixture.whenStable();

  expect(component.user.isLoading()).toBe(false);
  expect(component.user.value()?.id).toBe(42);
});

Because resource reads happen through signals, changes to userId automatically trigger re-computation in the test — no manual subscriptions needed.

Common mistakes

  • Reading signals outside request: signals read inside the loader body (not inside request) are not tracked. Always put the reactive inputs in the request function.
  • Forgetting that loaders get cancelled: if you have side effects inside the loader pipeline, they may be cancelled mid-flight. Keep loaders pure.
  • Using rxResource for one-off fire-and-forget calls: for mutations (POST/PUT/DELETE), prefer a direct HttpClient call. rxResource is for read models.
  • Mixing rxResource with toSignal: you do not need toSignalrxResource already exposes signals.
  • Relying on value() during initial render without hasValue(): the first evaluation returns undefined; use @if (resource.value(); as v) or hasValue() to guard.

rxResource and the evolving Angular reactivity model

rxResource is part of a broader shift in Angular: signals as the default primitive for reactive state, and resource-style APIs for async data. It sits next to linkedSignal, resource, and the ongoing zoneless push. For a full picture of where signals fit in enterprise Angular and where RxJS still earns its place, read Angular Signals vs RxJS: When to Use Each and Angular Signals State Management: Enterprise Guide.

FAQ

When did rxResource ship in Angular?

rxResource is part of the @angular/core/rxjs-interop package and matured through the Angular 19/20 cycle alongside the broader resource APIs. It is stable and production-ready in current Angular versions.

Do I still need HttpClient?

Yes. rxResource does not replace HttpClient — it wraps it. You still make HTTP calls with HttpClient; rxResource provides the reactive lifecycle around them.

Can I use rxResource without RxJS?

If your loader is a plain async function or Promise, use resource() instead. rxResource() exists specifically for Observable-returning loaders.

Does rxResource work with interceptors and guards?

Yes. Because the loader uses HttpClient directly, all interceptors, guards, and HTTP infrastructure work as usual.

How does rxResource compare to TanStack Query for Angular?

TanStack Query offers richer features like stale-while-revalidate, multi-level caching, and query invalidation. rxResource is simpler, framework-native, and SSR-integrated. For most Angular apps, start with rxResource and only reach for TanStack Query when you need advanced cache semantics.

Conclusion and next steps

rxResource is the cleanest way to load reactive async data in modern Angular: signal-driven inputs, Observable loader, automatic cancellation, SSR-aware, and testable. It replaces the effect() + HttpClient + manual signal update pattern with a single declarative primitive.

Pick one read model in your app that currently uses BehaviorSubject or manual effect() data loading and port it to rxResource. Measure the code reduction and the UX — you will likely get fewer moving parts and better SSR behavior. For the surrounding architecture, continue with Angular Signals State Management and NgRx in 2026: Do You Still Need It?.

Previous
Angular SSR Sitemap Generation: Build-Time Guide (2026)