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 |