httpResource (Angular 21)
What is httpResource?
Section intitulée « What is httpResource? »httpResource is an Angular 21 primitive from @angular/common/http that provides declarative HTTP data loading. It returns a Resource signal that automatiqueally handles loading states, errors, and provides reactive updates.
Why Use It?
Section intitulée « Why Use It? »- ✅ Declarative data loading - URL-based resource definition
- ✅ Automatique state management - Loading, error, and value states
- ✅ Basé sur signal - Returns
Resource<T>signal - ✅ Manual reload - Trigger refetch with
reload() - ✅ Type-safe - Full TypeScript support
- ✅ Simpler than HttpClient - Moins de boilerplate
httpResource vs HttpClient
Section intitulée « httpResource vs HttpClient »| Feature | HttpClient |
httpResource |
|---|---|---|
| API | Observable-based | Basé sur signal Resource |
| Loading state | Manual | Automatique |
| Error state | Manual try/catch | Automatique |
| Subscription | Required | Not required |
| Nettoyage | Manual unsubscribe | Automatique |
| Result type | Observable<T> |
Resource<T> |
| Reload | Re-call method | Call .reload() |
Basic Utilisation
Section intitulée « Basic Utilisation »import { Injectable, inject } from '@angular/core';import { HttpClient, httpResource } from '@angular/common/http';import { Album } from '../models/album.model';
@Injectable({ providedIn: 'root' })export class AlbumService { http = inject(HttpClient);
// Define the httpResource readonly _albumsResource = httpResource<Album[]>(() => { return 'http://localhost:3000/albums'; }, { defaultValue: [] });
// Expose as a getter get albumsResource() { return this._albumsResource; }
// Traditional HttpClient method (for comparison) findAll(): Observable<Album[]> { return this.http.get<Album[]>('http://localhost:3000/albums'); }}Key points:
- Returns a URL string from the loader function
- Automatiqueally creates a GET request
defaultValueprovides initial value before data loads- Resource is read-only, exposed via getter
import { Component, inject, computed } from '@angular/core';import { AlbumService } from './services/album.service';
@Component({ selector: 'app-album-list', standalone: true, template: ` <div> <!-- Loading state --> @if (albumsResource.isLoading()) { <p>Loading albums...</p> }
<!-- Error state --> @if (albumsResource.error()) { <p class="error">Error: {{ albumsResource.error()?.message }}</p> }
<!-- Data --> @if (albums(); as albumList) { <div class="album-grid"> @for (album of albumList; track album.id) { <div class="album-card"> <h3>{{ album.name }}</h3> <p>{{ album.artist }}</p> <p class="price">{{ album.price | currency }}</p> </div> } </div> }
<!-- Reload bouaun --> <button (click)="albumsResource.reload()"> 🔄 Reload Albums </button>
<!-- Status --> <p>Status: {{ albumsResource.status() }}</p> </div> `})export class AlbumListComponent { private albumService = inject(AlbumService);
// Get the resource from service albumsResource = this.albumService.albumsResource;
// Extract value using computed albums = computed(() => this.albumsResource.value());
// Resource provides: // - value() → Album[] | undefined // - isLoading() → booléen // - error() → Error | undefined // - status() → ResourceStatus // - reload() → void (trigger refetch)}Resource provides 4 key signals:
// 1. value() - The loaded dataconst albums = this.albumsResource.value();// Type: Album[] | undefined
// 2. isLoading() - Loading stateconst loading = this.albumsResource.isLoading();// Type: booléen
// 3. error() - Error stateconst err = this.albumsResource.error();// Type: Error | undefined
// 4. status() - Overall statusconst status = this.albumsResource.status();// Type: ResourceStatus// Values: ResourceStatus.Idle | ResourceStatus.Loading |// ResourceStatus.Error | ResourceStatus.ResolvedState transitions:
Initial → Idle (with defaultValue) ↓Loading → isLoading() = true ↓Success → value() = data, isLoading() = false, status() = Resolved ORError → error() = Error, isLoading() = false, status() = ErrorReload After Mutations
Section intitulée « Reload After Mutations »A common pattern is à reload the resource after creating, updating, or deleting items:
@Injectable({ providedIn: 'root' })export class AlbumService { http = inject(HttpClient);
// Resource for GET readonly _albumsResource = httpResource<Album[]>(() => { return 'http://localhost:3000/albums'; }, { defaultValue: [] });
get albumsResource() { return this._albumsResource; }
// Traditional methods for mutations save(album: Album): Observable<Album> { return this.http.post<Album>('http://localhost:3000/albums', album); }
update(album: Album): Observable<Album> { return this.http.put<Album>(`http://localhost:3000/albums/${album.id}`, album); }
delete(id: number): Observable<void> { return this.http.delete<void>(`http://localhost:3000/albums/${id}`); }}import { Component, inject } from '@angular/core';import { MatDialog, MatDialogRef } from '@angular/material/dialog';import { AlbumService } from './services/album.service';import { AddAlbumDialogComponent } from './add-album-dialog.component';
@Component({ selector: 'app-album-list', standalone: true, template: ` <button (click)="openAddDialog()">➕ Add Album</button>
@for (album of albums(); track album.id) { <div class="album-card"> <h3>{{ album.name }}</h3> <p>{{ album.artist }}</p> <button (click)="deleteAlbum(album.id)">🗑️ Delete</button> </div> } `})export class AlbumListComponent { private albumService = inject(AlbumService); private dialog = inject(MatDialog);
albumsResource = this.albumService.albumsResource; albums = computed(() => this.albumsResource.value());
openAddDialog() { const dialogRef = this.dialog.open(AddAlbumDialogComponent);
// Reload resource after dialog closes dialogRef.afterClosed().subscribe(() => { this.albumsResource.reload(); }); }
deleteAlbum(id: number) { this.albumService.delete(id).subscribe({ next: () => { // Reload resource after successful delete this.albumsResource.reload(); }, error: (err) => console.error('Delete failed:', err) }); }}Pattern:
- Perform mutation (POST, PUT, DELETE) using HttpClient
- On success, call
resource.reload()à refetch data - Resource automatiqueally updates all computed values
Avancé Patterns
Section intitulée « Avancé Patterns »Default Value
Section intitulée « Default Value »Fournir initial data while loading:
readonly albumsResource = httpResource<Album[]>(() => { return 'http://localhost:3000/albums';}, { defaultValue: [] // Start with empty tableau, not undefined});Avantages:
- Prevents
undefinederrors in templates - Can show placeholder data
- Smoother UX during initial load
Computed Derived Values
Section intitulée « Computed Derived Values »Utiliser computed() à derive additional signals:
export class AlbumListComponent { albumsResource = this.albumService.albumsResource;
// Extract value albums = computed(() => this.albumsResource.value() ?? []);
// Derived computations totalAlbums = computed(() => this.albums().length);
highlightedAlbums = computed(() => this.albums().filter(a => a.highlighted) );
totalPrice = computed(() => this.albums().reduce((sum, a) => sum + a.price, 0) );
averagePrice = computed(() => { const albums = this.albums(); return albums.length > 0 ? this.totalPrice() / albums.length : 0; });}Error Handling in Template
Section intitulée « Error Handling in Template »template: ` @if (albumsResource.error(); as err) { <div class="error-banner"> <h3>⚠️ Failed to load albums</h3> <p>{{ err.message }}</p> <button (click)="albumsResource.reload()"> 🔄 Retry </button> </div> }`Loading Skeleton
Section intitulée « Loading Skeleton »template: ` @if (albumsResource.isLoading()) { <div class="skeleton-grid"> @for (i of [1,2,3,4,5,6]; track i) { <div class="skeleton-card"> <div class="skeleton-line"></div> <div class="skeleton-line"></div> </div> } </div> }
@if (albums(); as albumList) { <!-- Real content --> }`Comparison with rxResource
Section intitulée « Comparison with rxResource »| Feature | httpResource |
rxResource |
|---|---|---|
| Import from | @angular/common/http |
@angular/core/rxjs-interop |
| Loader | Returns URL string | Returns Observable |
| HTTP Method | GET only | Any (via Observable) |
| Utiliser case | Simple GET requests | Complex Observable chains |
| Exemple | () => '/api/albums' |
() => http.get('/api/albums') |
When à use httpResource:
- ✅ Simple GET requests
- ✅ URL-based loading
- ✅ No complex RxJS opérateurs needed
When à use rxResource:
- ✅ POST, PUT, DELETE requests
- ✅ Complex Observable pipelines
- ✅ Need for RxJS opérateurs (map, filter, etc.)
- ✅ Multiple HTTP calls combined
httpResource vs HttpClient
Section intitulée « httpResource vs HttpClient »// ❌ HttpClient approach - Manual state management@Component({ selector: 'app-album-list', template: ` @if (loading) { <p>Loading...</p> } @if (error) { <p class="error">{{ error }}</p> } @if (!loading && !error) { @for (album of albums; track album.id) { <div>{{ album.name }}</div> } } `})export class AlbumListComponent implements OnInit, OnDestroy { albums: Album[] = []; loading = false; error: string | null = null; private destroy$ = new Subject<void>();
ngOnInit() { this.loadAlbums(); }
loadAlbums() { this.loading = true; this.error = null;
this.albumService.findAll() .pipe(takeUntil(this.destroy$)) .subscribe({ next: (data) => { this.albums = data; this.loading = false; }, error: (err) => { this.error = err.message; this.loading = false; } }); }
ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); }}Issues:
- Manual loading/error state management
- Need lifecycle hooks (OnInit, OnDestroy)
- Manual subscription management
- Manual cleanup with
takeUntil - More boilerplate code
// ✅ httpResource approach - Automatique state management@Component({ selector: 'app-album-list', template: ` @if (albumsResource.isLoading()) { <p>Loading...</p> } @if (albumsResource.error(); as err) { <p class="error">{{ err.message }}</p> } @for (album of albums(); track album.id) { <div>{{ album.name }}</div> } `})export class AlbumListComponent { private albumService = inject(AlbumService);
albumsResource = this.albumService.albumsResource; albums = computed(() => this.albumsResource.value() ?? []);
// That's it! No ngOnInit, no subscriptions, no cleanup}Avantages:
- ✅ Automatique state management
- ✅ No lifecycle hooks needed
- ✅ No manual subscriptions
- ✅ Automatique cleanup
- ✅ Less code, clesontr intent
Real-World Exemple
Section intitulée « Real-World Exemple »Complete example from student project (step-17):
@Injectable({ providedIn: 'root' })export class AlbumService { http = inject(HttpClient);
readonly _albumsResource = httpResource<Album[]>(() => { console.log('Loading albums...'); return 'http://localhost:3000/albums'; }, { defaultValue: [] });
get albumsResource() { return this._albumsResource; }
save(album: Album): Observable<Album> { return this.http.post<Album>('http://localhost:3000/albums', album); }
delete(id: number): Observable<void> { return this.http.delete<void>(`http://localhost:3000/albums/${id}`); }}@Component({ selector: 'app-album-list', standalone: true, imports: [CommonModule, CurrencyPipe], template: ` <div class="container"> <h1>Album Catalog</h1>
@if (albumsResource.isLoading()) { <div class="loading"> <p>Loading albums...</p> </div> }
@if (albumsResource.error(); as err) { <div class="error"> <p>Failed to load albums: {{ err.message }}</p> <button (click)="albumsResource.reload()">Retry</button> </div> }
@if (albums(); as albumList) { <div class="stats"> <p>Total Albums: {{ totalAlbums() }}</p> <p>Average Price: {{ averagePrice() | currency }}</p> </div>
<div class="album-grid"> @for (album of albumList; track album.id) { <div class="album-card"> <h3>{{ album.name }}</h3> <p class="artist">{{ album.artist }}</p> <p class="price">{{ album.price | currency }}</p> <div class="tags"> @for (tag of album.tags; track tag) { <span class="tag">{{ tag }}</span> } </div> <button (click)="deleteAlbum(album.id)">Delete</button> </div> } </div> } </div> `})export class AlbumListComponent { private albumService = inject(AlbumService);
albumsResource = this.albumService.albumsResource; albums = computed(() => this.albumsResource.value() ?? []);
totalAlbums = computed(() => this.albums().length);
averagePrice = computed(() => { const albums = this.albums(); if (albums.length === 0) return 0; return albums.reduce((sum, a) => sum + a.price, 0) / albums.length; });
deleteAlbum(id: number) { this.albumService.delete(id).subscribe({ next: () => this.albumsResource.reload(), error: (err) => console.error('Delete failed:', err) }); }}Bonnes Pratiques
Section intitulée « Bonnes Pratiques »- ✅ Utiliser
httpResourcefor simple GET requests - ✅ Fournir
defaultValueà avoid undefined errors - ✅ Garder resource private (
_albumsResource), expose via getter - ✅ Utiliser
computed()à derive values from resource - ✅ Call
reload()after mutations (POST, PUT, DELETE) - ✅ Handle errors gracefully in template with retry option
- ✅ Show loading states pour une meilleure UX
- ❌ Don’t mutate resource data directly
- ❌ Don’t use for POST/PUT/DELETE (use HttpClient instead)
- ❌ Don’t forget à provide type parameter:
httpResource<Album[]>()
Common Patterns
Section intitulée « Common Patterns »Pattern 1: Resource with Computed Filters
Section intitulée « Pattern 1: Resource with Computed Filters »albumsResource = this.albumService.albumsResource;albums = computed(() => this.albumsResource.value() ?? []);
// Filtered viewsrockAlbums = computed(() => this.albums().filter(a => a.tags.includes('Rock')));
highlightedAlbums = computed(() => this.albums().filter(a => a.highlighted));Pattern 2: Reload on User Action
Section intitulée « Pattern 2: Reload on User Action »refreshAlbums() { this.albumsResource.reload();}
createAlbum(album: Album) { this.albumService.save(album).subscribe({ next: () => this.albumsResource.reload() });}Pattern 3: Error Recovery
Section intitulée « Pattern 3: Error Recovery »template: ` @if (albumsResource.error()) { <div class="error-banner"> <p>Something went wrong!</p> <button (click)="albumsResource.reload()">Try Again</button> </div> }`Related Documentation
Section intitulée « Related Documentation »- rxResource - Observable-based resource API
- HttpClient - Traditional HTTP client
- State Management - Managing application state
- Album Model - Data model reference
- Signals - Signal fundamentals
Project Reference
Section intitulée « Project Reference »See this pattern in action:
- Service:
src/app/services/album.service.ts - Component:
src/app/components/albums/album-list/album-list.component.ts - Learning Path: Day 3, Module 3.6 - Resource API
Last Updated: December 2024 Angular Version: 21+ Status: Current best practice for declarative HTTP loading

