Skip to main content

Angular Resource API: Reactive Async Data Loading Beyond XMLHttpRequest

๐ŸŒ The Resource API โ€” Reactive Async Data in Angularโ€‹

The Resource API (resource, rxResource, and httpResource) is Angular's modern, signal-based way to load asynchronous data. Instead of manually wiring up XMLHttpRequest (or even HttpClient + subscribe) and juggling loading/error flags yourself, a resource gives you a single reactive object that exposes the data, loading status, and errors as signals.

๐Ÿงช Status: The Resource API is experimental/developer-preview (introduced in Angular 19, evolving through 20+). The API surface may change. Great for new apps and experimentation.


๐Ÿ•ฐ๏ธ The Old Way: XMLHttpRequestโ€‹

Before understanding the benefits, here's what raw async data loading traditionally looked like:

// โŒ The verbose, imperative XMLHttpRequest approach
export class OldUserComponent {
user: User | null = null;
loading = false;
error: string | null = null;

loadUser(id: number) {
this.loading = true;
this.error = null;

const xhr = new XMLHttpRequest();
xhr.open('GET', `/api/users/${id}`);
xhr.onload = () => {
this.loading = false;
if (xhr.status >= 200 && xhr.status < 300) {
this.user = JSON.parse(xhr.responseText);
} else {
this.error = `Request failed: ${xhr.status}`;
}
};
xhr.onerror = () => {
this.loading = false;
this.error = 'Network error';
};
xhr.send();
}
}

Problems with this approach:

  • ๐Ÿ”ด Manual state juggling โ€” you track loading, error, and data by hand.
  • ๐Ÿ”ด No cancellation โ€” changing the id mid-flight leaves stale requests racing.
  • ๐Ÿ”ด Imperative โ€” you must remember to call loadUser() at the right time.
  • ๐Ÿ”ด No reactivity โ€” it doesn't automatically re-fetch when inputs change.
  • ๐Ÿ”ด Verbose parsing & error handling for every single call.

โœจ The New Way: resource()โ€‹

A resource wraps all of that into one declarative, reactive object.

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

@Component({
selector: 'app-user',
template: `
@if (userResource.isLoading()) {
<p>Loadingโ€ฆ</p>
} @else if (userResource.error()) {
<p>Error: {{ userResource.error() }}</p>
} @else {
<h2>{{ userResource.value()?.name }}</h2>
<p>{{ userResource.value()?.email }}</p>
}

<button (click)="userId.set(userId() + 1)">Next User</button>
<button (click)="userResource.reload()">Reload</button>
`,
})
export class UserComponent {
userId = signal(1);

// ๐ŸŽฏ The resource re-runs automatically whenever `userId` changes
userResource = resource({
params: () => ({ id: this.userId() }), // reactive inputs
loader: async ({ params, abortSignal }) => { // async loader
const res = await fetch(`/api/users/${params.id}`, { signal: abortSignal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return (await res.json()) as User;
},
});
}

interface User {
id: number;
name: string;
email: string;
}

What you get for free:

  • โœ… userResource.value() โ€” the loaded data (a signal).
  • โœ… userResource.isLoading() โ€” loading state (a signal).
  • โœ… userResource.error() โ€” any thrown error (a signal).
  • โœ… userResource.status() โ€” the lifecycle status.
  • โœ… Automatic re-fetch when params change.
  • โœ… Automatic cancellation of the previous request via abortSignal.
  • โœ… reload() to manually refresh.

๐Ÿงฉ Anatomy of a Resourceโ€‹

Every resource exposes the same reactive surface:

MemberTypeDescription
value()Signal<T | undefined>The resolved data. undefined until first load completes.
isLoading()Signal<boolean>true while a request is in flight.
error()Signal<unknown>The error thrown by the loader, if any.
status()Signal<ResourceStatus>Current lifecycle state (see below).
hasValue()booleanType-guard: narrows value() to non-undefined.
reload()() => booleanManually re-run the loader.

Resource Status Lifecycleโ€‹

status() returns one of these states:

StatusMeaning
'idle'No request has started (e.g. params returned undefined).
'loading'First-time fetch in progress.
'reloading'Re-fetching while keeping the previous value.
'resolved'Data loaded successfully.
'error'Loader threw an error.

๐Ÿ”— params โ€” Reactive Inputs & Auto Re-fetchโ€‹

The params function is reactive: it reads signals, and whenever any of them change, the loader automatically re-runs. This replaces the imperative "call the function again" pattern.

export class ProductSearchComponent {
query = signal('');
category = signal('all');
page = signal(1);

results = resource({
// Re-runs whenever query, category, OR page changes
params: () => ({
q: this.query(),
category: this.category(),
page: this.page(),
}),
loader: async ({ params, abortSignal }) => {
const url = `/api/search?q=${params.q}&cat=${params.category}&page=${params.page}`;
const res = await fetch(url, { signal: abortSignal });
return res.json();
},
});
}

Skipping the Load Conditionallyโ€‹

Return undefined from params to keep the resource idle (no request):

userResource = resource({
// Don't fetch until we actually have an id
params: () => (this.userId() ? { id: this.userId() } : undefined),
loader: async ({ params }) => fetchUser(params.id),
});

๐Ÿšซ Automatic Cancellation with abortSignalโ€‹

Each time the loader re-runs, Angular aborts the previous request and passes a fresh AbortSignal. This eliminates race conditions where an older, slower response overwrites a newer one โ€” a common bug with manual XMLHttpRequest.

loader: async ({ params, abortSignal }) => {
// If params change again before this resolves,
// abortSignal fires and fetch() rejects โ€” no stale data.
const res = await fetch(`/api/users/${params.id}`, { signal: abortSignal });
return res.json();
}

๐Ÿ” rxResource โ€” For RxJS/Observable Loadersโ€‹

If your data source returns an Observable (e.g. HttpClient), use rxResource. The loader returns an Observable instead of a Promise, and Angular takes the first emitted value.

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

@Component({
selector: 'app-todo',
template: `
@if (todoResource.isLoading()) {
<p>Loading todoโ€ฆ</p>
} @else {
<h3>{{ todoResource.value()?.title }}</h3>
}
`,
})
export class TodoComponent {
private http = inject(HttpClient);
todoId = signal(1);

todoResource = rxResource({
params: () => ({ id: this.todoId() }),
stream: ({ params }) =>
this.http.get<Todo>(`https://jsonplaceholder.typicode.com/todos/${params.id}`),
});
}

interface Todo {
id: number;
title: string;
completed: boolean;
}

rxResource automatically subscribes and unsubscribes for you โ€” no manual subscribe()/unsubscribe() or takeUntilDestroyed().


๐ŸŒ httpResource โ€” The Most Concise Optionโ€‹

httpResource (from @angular/common/http) is purpose-built for HTTP. It combines HttpClient with the resource pattern, so you don't even write a loader โ€” just describe the request reactively.

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

@Component({
selector: 'app-user-http',
template: `
@if (user.isLoading()) {
<p>Loadingโ€ฆ</p>
} @else if (user.error()) {
<p>Failed to load user.</p>
} @else {
<h2>{{ user.value()?.name }}</h2>
}
`,
})
export class UserHttpComponent {
userId = signal(1);

// Simple GET โ€” reactive URL, typed response
user = httpResource<User>(() => `/api/users/${this.userId()}`);

// Advanced: full request config
usersList = httpResource<User[]>(() => ({
url: '/api/users',
method: 'GET',
params: { active: true, page: this.page() },
headers: { 'X-Custom': 'value' },
}));

page = signal(1);
}

httpResource benefits over raw HttpClient.subscribe():

  • โœ… No manual subscription management โ€” fully signal-based.
  • โœ… Built-in loading/error signals.
  • โœ… Automatic cancellation on input change.
  • โœ… Reactive URL/params โ€” change a signal, the request re-runs.
  • โœ… Goes through the same HTTP interceptors as HttpClient (auth, logging, etc.).

๐Ÿ’ก httpResource is for reading data (GET-style). For mutations (POST/PUT/DELETE triggered by user actions), keep using HttpClient directly.


โœ๏ธ Local Mutations with the Resource Valueโ€‹

The resource value() is a writable signal, so you can optimistically update it locally after a mutation without a full re-fetch:

export class ProfileComponent {
userResource = httpResource<User>(() => `/api/users/${this.userId()}`);
userId = signal(1);

updateName(newName: string) {
// Optimistic UI update
this.userResource.value.update((u) => (u ? { ...u, name: newName } : u));

// Persist to the server separately
this.http.put(`/api/users/${this.userId()}`, { name: newName }).subscribe();
}
}

๐Ÿ†š Comparison: XMLHttpRequest vs HttpClient vs Resource APIโ€‹

ConcernXMLHttpRequestHttpClient + subscribeResource API
Loading stateManual flagManual flagโœ… Built-in isLoading()
Error stateManualManual / catchErrorโœ… Built-in error()
CancellationManual abort()unsubscribe / takeUntilโœ… Automatic abortSignal
Re-fetch on input changeCall again manuallyRe-subscribe manuallyโœ… Automatic via params
Subscription cleanupN/AManualโœ… Automatic
Template integrationBind fieldsasync pipeโœ… Direct signals
Race-condition safetyโŒโš ๏ธ needs switchMapโœ… Built-in
Boilerplate๐Ÿ”ด High๐ŸŸ  Medium๐ŸŸข Low

โœ… Benefits Summaryโ€‹

  • Declarative, not imperative โ€” describe what data you need; Angular decides when to fetch.
  • Signal-native โ€” value, isLoading, error, status are all signals that plug straight into templates and computed.
  • Automatic re-fetching when reactive params change.
  • Automatic cancellation of stale requests โ†’ no race conditions.
  • No subscription leaks โ€” nothing to unsubscribe.
  • Less boilerplate โ€” replaces dozens of lines of manual state handling.
  • Works great with zoneless change detection and the rest of the signals ecosystem.

โš ๏ธ Caveats & Best Practicesโ€‹

  • ๐Ÿงช Experimental: the API is in developer preview โ€” pin your Angular version and watch the changelog.
  • ๐Ÿ“– Reads, not writes: resources model data loading. Use HttpClient for user-triggered mutations (POST/PUT/DELETE).
  • ๐Ÿ”— Always forward abortSignal to fetch/HTTP calls so cancellation actually works.
  • ๐ŸŽฏ Keep params pure โ€” only read signals and return a plain object; don't cause side effects there.
  • ๐Ÿšฆ Return undefined from params to defer a request until inputs are ready.
  • ๐Ÿงฎ Derive, don't duplicate: build computed signals from resource.value() instead of copying it into another signal.

๐ŸŽฏ Summaryโ€‹

The Resource API is Angular's answer to reactive async data loading. Where XMLHttpRequest forces you to manually manage requests, state, cancellation, and cleanup, resource() / rxResource() / httpResource() fold all of that into a single signal-based object that re-fetches on input changes, cancels stale requests, and exposes loading/error/data as signals โ€” dramatically less code with far fewer bugs. ๐Ÿš€