Angular httpResource vs resource: Signal-Shaped Async with Auto-Cancellation [2026]
HttpClient plus toSignal works, but it leaves you reimplementing the same boilerplate at every call site: loading state, error state, cancellation when inputs change, caching. The resource() and httpResource() APIs introduced in v19 wrap that boilerplate into a single primitive — signal-shaped async with cancellation, loading flags, error state, and refetch all built in.
This is lesson 6.3 of the Angular Tutorial. After this lesson, you'll reach for httpResource over raw HttpClient + toSignal for most fetching scenarios.
The shape #
httpResource takes a request factory and returns a resource object with signals for value, loading, error, and status:
import { Component, signal } from '@angular/core';
import { httpResource } from '@angular/common/http';
@Component({
selector: 'app-user-card',
template: `
@if (user.isLoading()) { <p>Loading...</p> }
@else if (user.error()) { <p>Error: {{ user.error()!.message }}</p> }
@else if (user.value(); as u) { <p>{{ u.name }}</p> }
`,
})
export class UserCardComponent {
userId = signal('1');
user = httpResource(() => ({
url: `https://jsonplaceholder.typicode.com/users/${this.userId()}`,
}));
}
Three mechanical facts:
- Factory function reads signals — the resource refetches automatically whenever any signal it reads changes
- Returns signals —
.value(),.isLoading(),.error(),.status()are all signal-shaped - Auto-cancels previous request — if
userIdchanges mid-flight, the in-flight request is aborted via AbortController
Change userId.set('2') and the resource fires a new request, cancels the previous, and updates .value() when the new response lands. No manual switchMap, no manual takeUntil.
The full request options #
The factory returns an object with all the request knobs:
user = httpResource(() => ({
url: `/api/users/${this.userId()}`,
method: 'GET', // default: GET
params: { include: 'posts' },
headers: { 'X-Trace': this.traceId() },
body: undefined,
withCredentials: false,
reportProgress: false,
context: new HttpContext().set(SKIP_AUTH, true),
}));
For POST/PUT/PATCH:
savedUser = httpResource(() => ({
url: '/api/users',
method: 'POST',
body: this.formValue(),
}));
The resource refetches whenever any tracked signal changes — including signals read inside body, params, or headers.
When the factory returns undefined #
If the factory returns undefined, no request fires. Useful for conditional fetching:
user = httpResource(() => {
const id = this.userId();
if (!id) return undefined; // skip fetch when id is empty
return { url: `/api/users/${id}` };
});
When userId() is empty, .value() stays undefined and no network request fires. When it gets a value, fetching kicks in.
The resource state machine #
Five signals expose the lifecycle:
| Signal | Type | What it tells you |
|---|---|---|
.value() |
T | undefined |
The successful response body, or undefined if not yet loaded |
.error() |
unknown |
Error from the request (typed as unknown — you cast) |
.isLoading() |
boolean |
True while a request is in flight |
.status() |
'idle' | 'loading' | 'reloading' | 'success' | 'error' |
Detailed state |
.hasValue() |
boolean |
True if .value() is not undefined |
Use .status() for fine-grained UI:
@switch (user.status()) {
@case ('idle') { <p>Pick a user</p> }
@case ('loading') { <p>Loading...</p> }
@case ('reloading') { <p>Refreshing...</p> }
@case ('success') { <user-card [user]="user.value()!" /> }
@case ('error') { <p>Error: {{ (user.error() as any)?.message }}</p> }
}
The reloading state lets you distinguish "loading the first time" from "loading a new value while we have a stale one" — useful for showing the stale data with a refresh spinner instead of unmounting and showing a generic loading state.
Manual refetch #
The resource exposes .reload() to refetch without changing inputs:
<button (click)="user.reload()">Refresh</button>
Useful for "click to reload" buttons or polling intervals.
Setting the value manually (optimistic updates) #
this.user.set(newUserObject); // updates .value() locally without a fetch
This is the optimistic-update pattern — write the new value to the resource's signal immediately, fire the actual save via a separate httpResource, on error roll back.
httpResource vs resource #
resource() is the generic primitive — accepts any async loader function. httpResource() is a specialization for HttpClient requests:
// Generic resource — works with anything async
user = resource({
params: () => ({ id: this.userId() }),
loader: async ({ params, abortSignal }) => {
const res = await fetch(`/api/users/${params.id}`, { signal: abortSignal });
return res.json();
},
});
// httpResource — HTTP-specific wrapper
user = httpResource(() => ({
url: `/api/users/${this.userId()}`,
}));
When to use which:
| Need | Use |
|---|---|
| HTTP request via HttpClient | httpResource() — gets interceptors, retries, response typing |
| Other async work (WebSocket query, IndexedDB read, Worker call) | resource() |
| Want the abort signal | Either — httpResource uses it internally, resource exposes it via abortSignal |
For 95% of fetching needs in a typical app, httpResource is the right tool.
Comparison: raw HttpClient + toSignal vs httpResource #
// Old pattern — HttpClient + toSignal
user$ = combineLatest([toObservable(this.userId)]).pipe(
switchMap(([id]) => this.http.get<User>(`/api/users/${id}`)),
);
user = toSignal(this.user$);
isLoading = ???; // need to roll your own
error = ???; // also roll your own
// New pattern — httpResource
user = httpResource(() => ({ url: `/api/users/${this.userId()}` }));
// .value(), .isLoading(), .error() all built in
For any fetching where loading + error states matter (most fetching), httpResource saves dozens of lines.
Common gotchas #
| Symptom | Cause | Fix |
|---|---|---|
| Resource fires twice on creation | Factory reads signals AND has side effects | Keep the factory pure — only return the request shape |
| Resource never refetches | Factory doesn't read the signal you expected | Confirm .set() calls update tracked signals; read each signal inside the factory |
.value() is typed any |
Forgot to declare the response type | httpResource<User>(() => ({ url: ... })) |
| Error doesn't fire | Server returned 2xx with error in body | Resource only catches actual HTTP errors; check status in .value() if API returns error in 200 |
| Optimistic update reverts on next fetch | .set() works, but next refetch overwrites it |
Use a separate signal for optimistic state OR wait until save completes before refetching |
What's next #
Lesson 6.4 covers error handling and retry patterns — the patterns to layer ON TOP of httpResource and HttpClient. Lesson 6.5 covers caching. Lesson 6.6 closes Module 6 with server-sent events.
Try it yourself #
A reactive search box driven by httpResource:
import { Component, signal } from '@angular/core';
import { httpResource, provideHttpClient, withFetch } from '@angular/common/http';
import { debounced } from '@angular/core/signals';
import { bootstrapApplication } from '@angular/platform-browser';
@Component({
selector: 'app-search',
template: `
<input [value]="query()" (input)="query.set($any($event.target).value)" placeholder="Search users" />
@if (results.isLoading()) { <p>Searching...</p> }
@else { <ul>@for (u of (results.value() ?? []); track u.id) { <li>{{ u.name }}</li> }</ul> }
`,
})
class SearchComponent {
query = signal('');
debouncedQuery = debounced(this.query, 300);
results = httpResource<{ id: number; name: string }[]>(() => {
const q = this.debouncedQuery();
if (!q) return undefined;
return { url: 'https://jsonplaceholder.typicode.com/users', params: { q } };
});
}
bootstrapApplication(SearchComponent, { providers: [provideHttpClient(withFetch())] });
Three primitives (signal, debounced, httpResource) → debounced search-as-you-type with cancellation. About 15 lines.
httpResource is dramatically less code than the equivalent RxJS pattern. For one-off mutations (POST a form, DELETE an item once), the imperative http.post(...).subscribe(...) pattern is still cleaner. The dividing line: if it reacts to inputs, use httpResource; if it’s a one-shot action, use raw HttpClient.Lesson 6.4 picks up error handling and retry patterns.
Up next in Angular
More from this topic
Enjoyed this article?
Get new Angular tutorials delivered. No spam — just code-first articles when they ship.


