BACK TO BLOG
ANGULAR 10 SEP 2026 · 8 MIN READ

Angular rxResource() Patterns Worth Knowing

The original guide to rxResource(), with code examples, comparison tables, and practical patterns.

Ali Gamal Ali

Lead Front-End Developer · Luftborn


// QUICK REACTIONS

Enjoyed this article? Leave a signal without writing a comment.

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

image

All examples use the Official Joke API through HttpClient. No manual Promise wrapping this time, HttpClient already hands back an Observable, and that plugs straight into the stream option.


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

Twitter / X LinkedIn Facebook Bluesky


Source: Original article on HackMD. Local review copy retrieved September 10, 2026.

// QUICK REACTIONS

Enjoyed this article? Leave a signal without writing a comment.


// DISCUSSION

Reader Comments (0)

Register or log in to leave a comment.

// NO COMMENTS YET