Chapter 8: HTTP & Async Operations
HTTP & Async Operations!
This is the chapter where your app stops being static and starts talking to real backends. We’ll fetch data from APIs, handle loading/error states elegantly, and keep everything reactive with signals — no more async pipe in most cases, no subscription management hell.
In 2026 (Angular 19–21+), the modern patterns have evolved a lot:
- Classic: HttpClient + toSignal() (still very useful)
- New experimental/stabilized APIs: resource(), rxResource(), and especially httpResource() (the most convenient for pure HTTP use cases)
httpResource() is now the recommended way for most HTTP fetching in signal-based apps — it’s built on top of HttpClient, handles cancellation, loading/error states as signals, and re-fetches automatically when inputs change.
Note: As of early 2026, resource() / rxResource() / httpResource() are stabilized or very close (moved from experimental in v19 → production-ready patterns in v20+). The Angular docs push httpResource for HTTP.
We’ll cover all three approaches with examples so you see the progression.
1. Classic: HttpClient + toSignal() (Still great baseline)
First, make sure HttpClientModule (or provideHttpClient()) is set up in your app.config.ts / main.ts:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// app.config.ts import { provideHttpClient } from '@angular/common/http'; export const appConfig = { providers: [ provideHttpClient(withFetch()), // withFetch() for modern fetch backend (recommended) // ... ] }; |
Example: Fetch a list of users from JSONPlaceholder (fake API)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
// src/app/user-list/user-list.component.ts import { Component, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { toSignal } from '@angular/core/rxjs-interop'; import { computed } from '@angular/core'; @Component({ selector: 'app-user-list', standalone: true, template: ` @if (usersLoading()) { <p>Loading users...</p> } @else if (usersError()) { <p>Error: {{ usersError()?.message }}</p> } @else { <ul> @for (user of users(); track user.id) { <li>{{ user.name }} ({{ user.email }})</li> } </ul> } ` }) export class UserListComponent { private http = inject(HttpClient); private users$ = this.http.get<User[]>('https://jsonplaceholder.typicode.com/users'); users = toSignal(this.users$, { initialValue: [] }); // Manual loading/error signals (a bit verbose) usersLoading = computed(() => !this.users()); usersError = computed(() => null); // you'd need extra logic for error } interface User { id: number; name: string; email: string; } |
→ This works, but you handle loading/error manually — not ideal.
Better: Use HttpClient with { observe: ‘response’ } or catchError, but still requires boilerplate.
2. Modern: httpResource() — The Recommended 2026 Way for HTTP
httpResource() is a reactive wrapper around HttpClient:
- Returns signals for value, isLoading, error, status
- Auto-cancels previous requests on new trigger
- Re-fetches when request signal changes
- Integrates perfectly with @if, computed, etc.
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
// user-list.component.ts (upgraded) import { Component, inject, signal } from '@angular/core'; import { httpResource } from '@angular/common/http'; // new import! @Component({ // ... template: ` @if (usersResource.isLoading()) { <p>Loading users... ⏳</p> } @else if (usersResource.hasError()) { <p style="color: red">Error: {{ usersResource.error()?.message }}</p> <button (click)="usersResource.reload()">Retry</button> } @else if (usersResource.hasValue()) { <ul> @for (user of usersResource.value(); track user.id) { <li>{{ user.name }} — {{ user.email }}</li> } </ul> } @else { <p>No data yet</p> } <button (click)="refreshUsers()">Refresh</button> ` }) export class UserListComponent { private usersResource = httpResource<User[]>(() => ({ url: 'https://jsonplaceholder.typicode.com/users' })); // Optional: manual refresh refreshUsers() { this.usersResource.reload(); } } |
Key signals from httpResource ref:
- .value() — the data (or undefined)
- .isLoading() / .hasValue() / .hasError()
- .error() — HttpErrorResponse or custom
- .status — ‘idle’ | ‘loading’ | ‘success’ | ‘error’
- .reload() — force refetch
Reactive / dependent fetch (re-fetches when signal changes)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
userId = signal<number | null>(null); // e.g. from route or input userResource = httpResource<User>(() => { if (!this.userId()) return null; // skip if no id return { url: `https://jsonplaceholder.typicode.com/users/${this.userId()}` }; }); |
→ Change userId.set(5) → auto fetch /users/5, cancels old request if pending
3. rxResource() — When You Already Have RxJS / Observables
Use this when your data comes from an RxJS stream (debounce, switchMap, combineLatest, etc.)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import { rxResource } from '@angular/core/rxjs-interop'; import { debounceTime, switchMap } from 'rxjs/operators'; searchTerm = signal(''); results = rxResource<string[]>(() => of(this.searchTerm()).pipe( debounceTime(400), switchMap(term => this.http.get<string[]>(`https://api.example.com/search?q=${term}`) ) ) ); |
→ Great bridge when you need RxJS operators but want signal output
4. resource() — Promise-based (fetch or custom async)
For non-HTTP (e.g. IndexedDB, WebSocket first message, Promise.all)
|
0 1 2 3 4 5 6 7 8 9 |
usersResource = resource<User[]>({ request: () => ({ /* some signal-based request object */ }), loader: ({request}) => fetch('...').then(r => r.json()) }); |
But for HTTP → prefer httpResource()
5. Quick Comparison Table (2026 teacher view)
| Approach | Best For | Loading/Error Built-in? | Auto-cancel? | Re-fetch on signal change? | RxJS needed? |
|---|---|---|---|---|---|
| HttpClient + toSignal | Simple cases, legacy code | Manual | Manual | Manual | Yes |
| httpResource() | Most HTTP fetches (recommended) | Yes (signals) | Yes | Yes | No |
| rxResource() | Complex streams (debounce, mergeMap) | Yes | Yes | Yes | Yes |
| resource() | Promise-based async (fetch, etc.) | Yes | No (manual) | Yes | No |
6. Mini Practice Task (try now!)
- Add provideHttpClient(withFetch()) if not already
- Create a PostDetailComponent with route /post/:id
- Use httpResource to fetch https://jsonplaceholder.typicode.com/posts/${id}
- Show title, body, loading spinner, error + retry button
- Bonus: Make id come from ActivatedRoute params via toSignal → auto-refetch on navigation
Template snippet:
|
0 1 2 3 4 5 6 7 8 9 10 11 |
@if (postResource.isLoading()) { <p>Fetching post...</p> } @else if (postResource.hasError()) { <p>Error! <button (click)="postResource.reload()">Retry</button></p> } @else { <h2>{{ postResource.value()?.title }}</h2> <p>{{ postResource.value()?.body }}</p> } |
You’ve now got modern, reactive data loading — clean templates, automatic states, cancellation.
Next is usually Forms (template-driven + reactive, with signals integration). Ready, or want to deepen HTTP (interceptors with signals, caching, polling, optimistic updates, error handling patterns)? Just tell me — we’re almost at full modern Angular apps! 🚀
