Async Data with resource
Loading data is one of the most common jobs in any app: fetch a list when a page opens, fetch again when the user types a new search, show a spinner while waiting and a message when something fails. Writing this by hand with subscribe() and several signals is repetitive and easy to get wrong.
Angular's resource APIs do it for you. A resource watches some signals, loads data whenever they change, and exposes the result, the loading state and any error as signals. resource, httpResource and rxResource are all stable in Angular 22.
resource(): loading with async/await
resource() takes two main options:
params: a function that reads signals and returns the values the request needs. When those signals change, the resource loads again.loader: anasyncfunction that receives the params and returns the data.
Here a PIN code lookup calls a backend with fetch whenever the user has typed all six digits:
import { Component, resource, signal } from '@angular/core';
interface Place {
pin: string;
district: string;
state: string;
}
@Component({
selector: 'app-pin-lookup',
template: `
<input #box [value]="pin()" (input)="pin.set(box.value)" maxlength="6" placeholder="PIN code" />
@if (place.isLoading()) {
<p>Looking up {{ pin() }}...</p>
} @else if (place.error()) {
<p class="error">Could not find that PIN code.</p>
} @else if (place.hasValue()) {
<p>{{ place.value().district }}, {{ place.value().state }}</p>
} @else {
<p>Enter all 6 digits.</p>
}
`,
})
export class PinLookup {
protected pin = signal('302001');
protected place = resource({
params: () => (this.pin().length === 6 ? this.pin() : undefined),
loader: async ({ params, abortSignal }) => {
const response = await fetch(`/api/pincode/${params}`, { signal: abortSignal });
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return (await response.json()) as Place;
},
});
}If the server answers { "pin": "302001", "district": "Jaipur", "state": "Rajasthan" }, the page shows:
Looking up 302001... Jaipur, Rajasthan
Some details make this robust:
- When
paramsreturnsundefined, the resource does not load at all and its status is'idle'. That is how "wait until six digits" works without anyifin the loader. abortSignalis cancelled when the params change before the previous request finishes. Passing it tofetchstops old, unneeded requests, so a slow answer for an old PIN can never overwrite the answer for the new one.- Throwing in the loader puts the resource into the error state.
The state signals
Every resource exposes its state as signals:
| Signal or method | Meaning |
|---|---|
value() | The loaded data (or the default value) |
hasValue() | true when a value is available |
isLoading() | true while a request is running |
error() | The error, if the last load failed |
status() | 'idle', 'loading', 'reloading', 'resolved', 'error' or 'local' |
reload() | Runs the loader again with the same params |
set() / update() | Change the value locally, for example after an edit (status becomes 'local') |
httpResource(): resources on top of HttpClient
Most Angular apps use HttpClient, with its interceptors for auth headers and logging. httpResource() creates a resource that sends its requests through HttpClient (so remember provideHttpClient()). You only describe the request, as a URL string or an object, and it is re-sent whenever the signals it reads change:
import { httpResource } from '@angular/common/http';
import { Component, computed, signal } from '@angular/core';
interface MenuItem {
id: number;
name: string;
price: number;
}
@Component({
selector: 'app-menu-search',
template: `
<input #q (input)="term.set(q.value)" placeholder="Search the menu" />
@if (menu.error()) {
<p class="error">Menu unavailable. <button (click)="menu.reload()">Try again</button></p>
} @else {
@if (menu.isLoading()) {
<p>Loading...</p>
}
<ul>
@for (item of menu.value(); track item.id) {
<li>{{ item.name }} - ₹{{ item.price }}</li>
} @empty {
<li>No dishes found.</li>
}
</ul>
<p>{{ count() }} dishes</p>
}
`,
})
export class MenuSearch {
protected term = signal('');
protected menu = httpResource<MenuItem[]>(
() => ({ url: '/api/menu', params: { q: this.term() } }),
{ defaultValue: [] },
);
protected count = computed(() => this.menu.value().length);
}The first request goes to /api/menu?q=. Typing "dosa" cancels any request still running and sends /api/menu?q=dosa:
Loading... No dishes found. 0 dishes Masala Dosa - ₹80 Rava Dosa - ₹90 2 dishes
While the new request runs, value() falls back to the defaultValue (an empty list), which is why "No dishes found" briefly appears under "Loading...". defaultValue also means value() is never undefined, so count needs no extra checks. If the server fails, the error message and a "Try again" button appear; reload() sends the same request again.
httpResource.text(), httpResource.blob() and httpResource.arrayBuffer() load other response types, and an HttpResourceRef also offers statusCode() and headers() signals.
rxResource(): when you already have an observable
If your service already returns observables, as the MenuApi from HttpClient does, use rxResource from @angular/core/rxjs-interop. It takes a stream function that returns an observable instead of a promise:
import { Component, inject, signal } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { MenuApi } from './menu-api';
@Component({
selector: 'app-veg-menu',
template: `<p>{{ vegItems.value()?.length ?? 0 }} veg dishes</p>`,
})
export class VegMenu {
private api = inject(MenuApi);
protected term = signal('');
protected vegItems = rxResource({
params: () => this.term(),
stream: ({ params }) => this.api.search(params, true),
});
}When the params change, rxResource unsubscribes from the old observable for you.
Which one should I use?
- httpResource for reading JSON from your API in a component: shortest code, uses your interceptors.
- resource for any other async source:
fetch, IndexedDB, a third-party SDK that returns promises. - rxResource when you already have an observable-based service.
- Plain
HttpClientcalls withsubscribe()(orfirstValueFrom) for writes such as POST, PUT and DELETE. Resources are designed for reading data that depends on signals; do not trigger a payment or an order by changing a signal.
Common mistakes
- Reading
value()in the error state. It throws. Checkerror()orhasValue()first. - Forgetting
abortSignalin aresourceloader. Old requests then keep running and waste data, especially on slow mobile networks. - Using a resource for writes. Changing a signal should not place an order. Call
HttpClient.post()directly from an event handler. - Making every keystroke a request. For search boxes, wait for a short pause in typing. Angular 22 adds an experimental
debounced()helper for this; RxJSdebounceTime(see the next lesson) is the stable option.
Key points
- Resources load async data into signals and reload automatically when the signals in
paramschange. resourceuses anasyncloader,httpResourceusesHttpClient, andrxResourceuses an observable stream.- Returning
undefinedfromparamskeeps a resource idle;abortSignalcancels outdated requests. - Use
isLoading(),error(),hasValue(),status()andreload()to build loading and error states. defaultValuekeepsvalue()defined; never readvalue()while the resource is in the error state.
Exercise
Build a TrainStatus component with a trainNo signal (start with '12301'). Use httpResource to load /api/trains/{trainNo}/status, which returns { name: string; delayMinutes: number }. Do not send a request unless the train number has exactly 5 digits. Show "Loading...", an error message with a retry button, or "NAME is running X minutes late" (or "on time" when the delay is 0).
Show solution
import { httpResource } from '@angular/common/http';
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-train-status',
template: `
<input #t [value]="trainNo()" (input)="trainNo.set(t.value)" />
@if (status.isLoading()) {
<p>Loading...</p>
} @else if (status.error()) {
<p>Status not available. <button (click)="status.reload()">Retry</button></p>
} @else if (status.hasValue()) {
@let s = status.value();
<p>{{ s.name }} is {{ s.delayMinutes === 0 ? 'on time' : 'running ' + s.delayMinutes + ' minutes late' }}</p>
}
`,
})
export class TrainStatus {
protected trainNo = signal('12301');
protected status = httpResource<{ name: string; delayMinutes: number }>(() =>
/^\d{5}$/.test(this.trainNo()) ? `/api/trains/${this.trainNo()}/status` : undefined,
);
}Returning undefined from the request function keeps the resource idle for incomplete numbers. The template checks error() before reading value(), so a failed request never throws.
Quick Quiz
1. What happens when the params function of a resource returns undefined?
undefined params mean "nothing to load yet", so the status is 'idle'.
2. Which API sends its requests through HttpClient and its interceptors?
httpResource is built on HttpClient.
3. What does reading value() do while a resource is in the error state?
Check error() or hasValue() before reading value().
4. Which task should NOT be done with a resource?
Resources are for reading data; writes belong in event handlers that call HttpClient directly.
Learn Angular with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
