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, anddataby hand. - ๐ด No cancellation โ changing the
idmid-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
paramschange. - โ
Automatic cancellation of the previous request via
abortSignal. - โ
reload()to manually refresh.
๐งฉ Anatomy of a Resourceโ
Every resource exposes the same reactive surface:
| Member | Type | Description |
|---|---|---|
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() | boolean | Type-guard: narrows value() to non-undefined. |
reload() | () => boolean | Manually re-run the loader. |
Resource Status Lifecycleโ
status() returns one of these states:
| Status | Meaning |
|---|---|
'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;
}
rxResourceautomatically subscribes and unsubscribes for you โ no manualsubscribe()/unsubscribe()ortakeUntilDestroyed().
๐ 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.).
๐ก
httpResourceis for reading data (GET-style). For mutations (POST/PUT/DELETE triggered by user actions), keep usingHttpClientdirectly.
โ๏ธ 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โ
| Concern | XMLHttpRequest | HttpClient + subscribe | Resource API |
|---|---|---|---|
| Loading state | Manual flag | Manual flag | โ
Built-in isLoading() |
| Error state | Manual | Manual / catchError | โ
Built-in error() |
| Cancellation | Manual abort() | unsubscribe / takeUntil | โ
Automatic abortSignal |
| Re-fetch on input change | Call again manually | Re-subscribe manually | โ
Automatic via params |
| Subscription cleanup | N/A | Manual | โ Automatic |
| Template integration | Bind fields | async 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,statusare all signals that plug straight into templates andcomputed. - Automatic re-fetching when reactive
paramschange. - 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
HttpClientfor user-triggered mutations (POST/PUT/DELETE). - ๐ Always forward
abortSignaltofetch/HTTP calls so cancellation actually works. - ๐ฏ Keep
paramspure โ only read signals and return a plain object; don't cause side effects there. - ๐ฆ Return
undefinedfromparamsto defer a request until inputs are ready. - ๐งฎ Derive, don't duplicate: build
computedsignals fromresource.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. ๐