DEV Community

Cover image for Angular's Resource APIs Are Finally Stable - Say Goodbye to Manual Loading States - What Changes in NG 22 (Part 7)
Rajat
Rajat

Posted on

Angular's Resource APIs Are Finally Stable - Say Goodbye to Manual Loading States - What Changes in NG 22 (Part 7)

How resource(), httpResource(), and rxResource() in Angular 22 replace your isLoading flags with a proper signal-based data model

How many times have you written the exact same three lines in a component that fetches data — set isLoading to true, make the call, set it back to false in a finally block, and hope you didn't forget the error flag somewhere? If you've been doing Angular for more than a few months, the honest answer is probably "too many to count."

With Angular 22, that pattern finally has an official, stable answer. resource(), httpResource(), and rxResource() — which have been sitting in developer preview for a while — graduated to stable APIs in this release, alongside Signal Forms and a switch to OnPush as the default change detection strategy. Angular is clearly betting hard on a signals-first future, and resources are the missing piece that makes asynchronous data feel as natural as a computed signal.

In this article, we'll go hands-on with all three resource functions. By the end, you'll know:

  • Why resources exist and what problem they actually solve
  • How to fetch data with httpResource() using the new control flow syntax
  • How to chain dependent resources without nesting subscriptions
  • How Angular automatically cancels stale requests for you
  • How to write unit tests for components that use resources
  • A handful of bonus tricks the docs don't shout about

If you've followed my previous pieces on standalone components and signal-based state management, this one slots in nicely as the "what do I do with server data" chapter. If this is your first stop, no worries — everything here is self-contained.

One more thing before we start: if you've found pieces like this useful before, following me here on Medium means you won't miss the next one. I write about Angular internals roughly once a week, and I'd rather you find out about it than scroll past it in six months.

Before we dive into the examples, a quick note: the code snippets provided here are meant purely for understanding the concept. Some syntax shown may reflect patterns from earlier Angular versions. Always refer to the official documentation for the most current API and syntax.

Why resources exist in the first place

Before resources, fetching data reactively in Angular usually meant one of two things: subscribing to an HttpClient observable manually inside a component (and remembering to unsubscribe), or wiring up toSignal() on top of an RxJS pipeline just to get a value your template could read. Neither approach knew anything about loading or error states out of the box — you built that bookkeeping yourself, in every component, over and over.

A resource flips that around. You give it a reactive request — usually a function that reads one or more signals — and it gives you back an object that always knows whether it's idle, loading, resolved, or errored, without you touching a boolean anywhere. According to the official Angular v22 release notes, this stabilization is part of a broader push toward "a production-ready reactive foundation" across the framework, not just an isolated feature.

Meet httpResource()

The friendliest entry point is httpResource(), which lives in @angular/common/http. You pass it a function that returns a request description, and it takes care of firing the HTTP call whenever a signal inside that function changes.

import { httpResource } from '@angular/common/http';
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';

interface Flight {
  id: number;
  from: string;
  to: string;
  date: string;
}

@Component({
  selector: 'app-flight-search',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <input
      [value]="filter().from"
      (input)="updateFrom($event)"
      placeholder="From"
    />

    @switch (flightsResource.status()) {
      @case ('loading') {
        <p role="status">Loading flights…</p>
      }
      @case ('error') {
        <p>Something went wrong: {{ flightsResource.error() }}</p>
      }
      @case ('resolved') {
        <ul>
          @for (flight of flightsResource.value(); track flight.id) {
            <li>{{ flight.from }} → {{ flight.to }} on {{ flight.date }}</li>
          }
        </ul>
      }
      @default {
        <p>Type a city to search.</p>
      }
    }
  `,
})
export class FlightSearch {
  protected readonly filter = signal({ from: '', to: '' });

  protected readonly flightsResource = httpResource<Flight[]>(
    () => {
      const { from, to } = this.filter();
      if (!from || !to) {
        return undefined; // no request until both fields are filled
      }
      return {
        url: 'https://api.example.com/flights',
        params: { from, to },
      };
    },
    { defaultValue: [] },
  );

  protected updateFrom(event: Event): void {
    const value = (event.target as HTMLInputElement).value;
    this.filter.update((current) => ({ ...current, from: value }));
  }
}
Enter fullscreen mode Exit fullscreen mode

A few things worth calling out here, because they trip people up the first time:

  • The generic Flight[] tells httpResource what shape to expect back, so flightsResource.value() is properly typed instead of unknown.
  • Returning undefined from the request function tells the resource "don't fetch anything right now" — no need for a separate guard flag.
  • defaultValue: [] means your template never has to deal with undefined before the first successful load.
  • status() gives you a more granular view than a single boolean: idle, loading, reloading, error, resolved, or local (for values you've overridden manually). The @switch block above uses exactly those states, and since Angular 22 you can pair this with @default never(...) if you want the compiler to yell at you when a new status value shows up and you forgot to handle it.

Ever built the exact same loading/error/data switch by hand in three different components in the same sprint? Tell me how you usually structure it — I'm curious whether most people reach for a shared directive or just copy-paste it.

Chaining resources without nesting subscriptions

A pattern that used to mean nested switchMap calls — "load the user, then load their posts" — is now handled by the chain() helper available inside a resource's params function. Instead of manually threading loading and error states between two resources, chain() propagates them for you.

import { httpResource } from '@angular/common/http';

interface User {
  id: number;
  authorId: number;
}

interface Post {
  id: number;
  title: string;
}

const userResource = httpResource<User>(() => `/api/users/${userId()}`);

const postsResource = httpResource<Post[]>({
  params: ({ chain }) => {
    const user = chain(userResource);
    return { url: `/api/posts`, params: { authorId: user.authorId } };
  },
});
Enter fullscreen mode Exit fullscreen mode

If userResource is still loading, postsResource automatically reports itself as loading too — you never see a flash of "no posts found" while the user is still in flight. If userResource errors out, postsResource errors with a dedicated ResourceDependencyError instead of silently trying to fetch posts for a user that doesn't exist. That's a genuinely nice ergonomic win over manually forwarding state between two separate subscriptions.

You don't need to worry about race conditions anymore

Here's the part that usually gets a "wait, really?" reaction from people used to RxJS. If your request signal changes quickly — someone typing in a search box, for instance — and a newer request resolves before an older one, resources automatically discard the stale response. Only the result of the most recent request is ever applied, which mirrors what switchMap gives you in RxJS, except you get it for free with no operator to remember.

This matters more than it sounds. Race-condition bugs from out-of-order responses are one of those things that pass code review because they're invisible until a slow network connection surfaces them in production.

rxResource() — for when you're not ready to leave RxJS behind

Not every data source maps cleanly to a single HTTP call. If you're wrapping a WebSocket, a polling interval, or an existing RxJS-based service, rxResource() lets you build a resource around any Observable-returning function instead of a plain HTTP request.

import { rxResource } from '@angular/core/rxjs-interop';
import { inject, signal } from '@angular/core';
import { FlightService } from './flight.service';

export class FlightSearchWithRx {
  private readonly flightService = inject(FlightService);
  protected readonly airportCode = signal('HAM');

  protected readonly flightsResource = rxResource({
    params: () => this.airportCode(),
    stream: ({ params }) => this.flightService.findByAirport(params),
  });
}
Enter fullscreen mode Exit fullscreen mode

The template usage is identical to httpResource — same value(), error(), isLoading(), and status() signals. The only difference is the source: instead of describing a request object, you hand it an Observable factory. That interop is deliberate. Angular isn't asking you to rip out your existing RxJS-based services; it's giving you a bridge so the rest of your component can stay signal-first.

The lowest-level option: plain resource()

If your async source isn't HTTP or RxJS at all — a Promise-based SDK call, an IndexedDB read, whatever — resource() itself is the primitive both of the above are built on.

import { resource, signal } from '@angular/core';

const userId = signal(42);

const userResource = resource({
  params: () => userId(),
  loader: async ({ params, abortSignal }) => {
    const response = await fetch(`/api/users/${params}`, {
      signal: abortSignal,
    });
    if (!response.ok) {
      throw new Error(`Failed to load user ${params}`);
    }
    return response.json();
  },
});
Enter fullscreen mode Exit fullscreen mode

Notice the abortSignal argument — resources hand you a standard AbortSignal so you can actually cancel an in-flight fetch call when a newer request supersedes it, rather than just ignoring the stale response once it lands. That's the mechanism behind the automatic race-condition handling described above.

Want more tips like this on modern Angular internals? Drop a follow, or subscribe to my newsletter — I'd rather send it to your inbox than hope you catch it in the feed.

Testing components that use resources

A common worry with any new async primitive is "how am I supposed to test this without a mountain of fake timers." The good news is resources built on httpResource still go through HttpClient under the hood, so the standard HttpTestingController workflow applies with only one adjustment: you need to wait for the resource to settle instead of asserting immediately.

import { TestBed } from '@angular/core/testing';
import {
  HttpTestingController,
  provideHttpClientTesting,
} from '@angular/common/http/testing';
import { provideHttpClient } from '@angular/common/http';
import { FlightSearch } from './flight-search';

describe('FlightSearch', () => {
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [provideHttpClient(), provideHttpClientTesting()],
    });
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    httpMock.verify();
  });

  it('loads flights once both filter fields are set', async () => {
    const fixture = TestBed.createComponent(FlightSearch);
    const component = fixture.componentInstance;

    component['filter'].set({ from: 'Hamburg', to: 'Graz' });
    fixture.detectChanges();

    const req = httpMock.expectOne((request) =>
      request.url.includes('/flights'),
    );
    req.flush([{ id: 1, from: 'Hamburg', to: 'Graz', date: '2026-08-01' }]);

    await fixture.whenStable();
    fixture.detectChanges();

    expect(component['flightsResource'].value().length).toBe(1);
    expect(component['flightsResource'].status()).toBe('resolved');
  });

  it('does not fire a request when a filter field is empty', () => {
    const fixture = TestBed.createComponent(FlightSearch);
    fixture.detectChanges();

    httpMock.expectNone((request) => request.url.includes('/flights'));
  });
});
Enter fullscreen mode Exit fullscreen mode

The key line is await fixture.whenStable(). Resources resolve asynchronously even after the mocked response is flushed, so reaching for fixture.detectChanges() alone right after req.flush(...) will catch you off guard with a still-pending resource. If you'd rather assert on isLoading() mid-flight, just check it before calling flush — the resource will report loading (or reloading, if it's a subsequent fetch) until the mocked response lands.

Ever had a test pass locally and flake in CI because of exactly this kind of async timing gap? I've lost a Friday afternoon to that one — what's your go-to way of catching it before it ships?

Bonus tips

A few extras that didn't fit neatly into the sections above but are worth keeping in your back pocket:

  • Cache resources across SSR and the client. Give a resource an id in its options and, per the Angular v22 changelog, it can be cached so the value rendered on the server is reused on the client instead of re-fetching immediately after hydration.
  • debounced() for anything that isn't a form field. Signal Forms has built-in debouncing, but if you need to debounce a plain signal that feeds a resource — a search box that isn't part of a form, for instance — the new debounced() function returns a resource-shaped value you can read with .value() and .isLoading() while it settles.
  • Resource composition via snapshots. If you need to derive a new resource from an existing one — filtering its value, or keeping the previous value visible while reloading instead of flashing undefinedresourceFromSnapshots() combined with linkedSignal on a resource's snapshot() lets you do that without touching the original loading logic. It's a more advanced pattern, but it's the cleanest way to avoid duplicating fetch logic just to reshape data.
  • Pair resources with OnPush deliberately, even though it's now the default. Since Angular 22 makes OnPush the default change detection strategy, a component driven entirely by resource and other signals gets fully reactive rendering with zero manual markForCheck() calls — which is also exactly what you want in a zoneless application.
  • Don't forget the abortSignal if you roll your own loader. It's easy to write a resource() loader that ignores abortSignal because the happy path works fine without it. You'll only notice the missing cancellation when a slow request keeps running after a newer one has already superseded it.

Recap

Resources give Angular a proper answer to a problem every one of us has solved manually at least a dozen times: tracking the state of an asynchronous request without scattering boolean flags across a component. With Angular 22, resource(), httpResource(), and rxResource() are no longer an experiment — they're a stable, production-ready part of the framework, complete with automatic stale-request cancellation, resource chaining via chain(), and SSR-aware caching.

If you're starting a new Angular 22 project, or even just adding a new feature to an existing one, reaching for httpResource() instead of a hand-rolled subscription is a genuinely small change that removes a surprising amount of boilerplate. Give it a try in your next component and see how much of your isLoading bookkeeping just disappears.


What did you think?
Did this approach match how you are solving it, or do you have a different take? Drop a comment — I genuinely read every single one.

Found this helpful?
If this saved you even a few minutes of debugging or confusion, hit that clap button so others can find it too. It really does make a difference.

Want more tips like this?
I share one practical dev insight every week. Follow me here on Medium or subscribe to my newsletter so you never miss one.

Let us connect — find me on LinkedIn or GitHub and let us keep the conversation going.


Follow Me for More Angular & Frontend Goodness:

I regularly share hands-on tutorials, clean code tips, scalable frontend architecture, and real-world problem-solving guides.

  • 💼 LinkedIn — Let’s connect professionally
  • 🎥 Threads — Short-form frontend insights
  • 🐦 X (Twitter) — Developer banter + code snippets
  • 👥 BlueSky — Stay up to date on frontend trends
  • 🌟 GitHub Projects — Explore code in action
  • 🌐 Website — Everything in one place
  • 📚 Medium Blog — Long-form content and deep-dives
  • 💬 Dev Blog — Free Long-form content and deep-dives
  • ✉️ Substack — Weekly frontend stories & curated resources
  • 🧩 Portfolio — Projects, talks, and recognitions
  • ✍️ Hashnode — Developer blog posts & tech discussions
  • ✍️ Reddit — Developer blog posts & tech discussions

Top comments (0)