Resource API
Most components don’t just hold state — they fetch it. A user picks an id, you load a record; a search box changes, you query an API. Wiring that up traditionally means juggling subscriptions, loading flags, error variables, and cancellation of stale requests. The resource() and rxResource() APIs (experimental, introduced in Angular 19) fold all of that into a single reactive primitive: you declare what the request depends on and how to load it, and Angular gives you a signal-based bundle of value, status, and error that re-fetches automatically when its inputs change.
What a resource gives you
A resource ties an asynchronous loader to a set of reactive request parameters. Whenever a signal read inside the request changes, the previous in-flight load is cancelled and a new one starts. The result is exposed entirely through signals, so it composes cleanly with computed(), effect(), and your template’s @if/@for control flow.
| Member | Type | Description |
|---|---|---|
value() | Signal<T | undefined> | The loaded data, or undefined before the first success |
status() | Signal<ResourceStatus> | 'idle', 'loading', 'reloading', 'resolved', or 'error' |
error() | Signal<unknown> | The thrown error, when status is 'error' |
isLoading() | Signal<boolean> | True during the initial load and reloads |
reload() | () => boolean | Manually re-runs the loader with the current request |
Using resource()
The resource() function takes a request function (the reactive inputs) and a loader (an async function that returns a Promise). The loader receives the resolved request value plus an abortSignal you can pass to fetch for automatic cancellation.
import { Component, signal, resource } from '@angular/core';
interface User {
id: number;
name: string;
email: string;
}
@Component({
selector: 'app-user-card',
standalone: true,
template: `
<input type="number" [value]="userId()" (input)="setId($event)" />
@if (userResource.isLoading()) {
<p>Loading…</p>
} @else if (userResource.error()) {
<p class="error">Failed to load user.</p>
} @else if (userResource.value(); as user) {
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
}
`,
})
export class UserCardComponent {
readonly userId = signal(1);
readonly userResource = resource({
// Reactive inputs — changing userId() triggers a reload.
request: () => ({ id: this.userId() }),
// Loader runs whenever request changes; old requests are aborted.
loader: async ({ request, abortSignal }) => {
const res = await fetch(`/api/users/${request.id}`, { signal: abortSignal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return (await res.json()) as User;
},
});
setId(event: Event) {
this.userId.set(Number((event.target as HTMLInputElement).value));
}
}
When the user types a new id, request() produces a fresh object, Angular aborts the previous fetch, and the loader re-runs. The template reacts to isLoading(), error(), and value() without a single manual subscription.
Output:
status() -> "loading" // initial fetch
status() -> "resolved" // value() now holds the User
status() -> "reloading" // user changed the id; old fetch aborted
status() -> "resolved" // new User loaded
Tip: Returning
undefinedfrom therequestfunction tells the resource the inputs aren’t ready yet — the loader is skipped andstatus()stays'idle'. This is the clean way to defer loading until, say, an id actually exists.
Using rxResource()
If your data layer already returns Observables — for example HttpClient — use rxResource() from @angular/core/rxjs-interop. It’s identical in shape but the loader returns an Observable instead of a Promise. The resource takes the first emitted value and unsubscribes automatically; changing the request cancels the in-flight stream.
import { Component, signal, inject } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-product',
standalone: true,
template: `
@if (product.value(); as p) {
<h2>{{ p.name }} — {{ p.price | currency }}</h2>
}
`,
})
export class ProductComponent {
private http = inject(HttpClient);
readonly productId = signal(42);
readonly product = rxResource({
request: () => this.productId(),
loader: ({ request }) =>
this.http.get<{ name: string; price: number }>(`/api/products/${request}`),
});
}
resource() vs rxResource()
| Aspect | resource() | rxResource() |
|---|---|---|
| Loader returns | Promise<T> | Observable<T> |
| Cancellation | Via abortSignal | Auto-unsubscribe on new request |
| Import | @angular/core | @angular/core/rxjs-interop |
| Best for | fetch, async/await code | HttpClient, RxJS pipelines |
| Emissions used | The resolved promise | The first emission only |
Updating and reloading
The value signal is read-only by default, but you can optimistically mutate it via the update/set helpers when the resource is defined as writable, and you can force a refresh with reload():
refresh() {
// Re-runs the loader with the current request; returns false if already loading.
this.userResource.reload();
}
Warning: These APIs are experimental. Names like
ResourceStatusand the exact loader signature may shift between minor releases — pin your Angular version and check the changelog before upgrading in production.
Best practices
- Keep the
requestfunction tiny and pure: read only the signals the load truly depends on, so you don’t trigger spurious re-fetches. - Always thread
abortSignalintofetchso stale, superseded requests are cancelled instead of racing. - Return
undefinedfromrequestto defer loading until inputs are valid, rather than guarding inside the loader. - Prefer
rxResource()when you’re already usingHttpClient— you get cancellation and interceptors for free. - Drive your template from
status()/isLoading()/error()with@if/@else if; avoid maintaining parallel boolean flags. - Treat
value()as possiblyundefinedon first render and narrow it with@if (res.value(); as v). - Because it’s experimental, isolate resource usage behind a small service so a future API change touches one file.