Aller au contenu principal

httpResource (Angular 21)

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?

  • 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

FeatureHttpClienthttpResource
APIObservable-basedBasé sur signal Resource
Loading stateManualAutomatique
Error stateManual try/catchAutomatique
SubscriptionRequiredNot required
NettoyageManual unsubscribeAutomatique
Result typeObservable<T>Resource<T>
ReloadRe-call methodCall .reload()

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
  • defaultValue provides initial value before data loads
  • Resource is read-only, exposed via getter

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}`);
}
}

Avancé Patterns

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 undefined errors in templates
  • Can show placeholder data
  • Smoother UX during initial load

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

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

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

FeaturehttpResourcerxResource
Import from@angular/common/http@angular/core/rxjs-interop
LoaderReturns URL stringReturns Observable
HTTP MethodGET onlyAny (via Observable)
Utiliser caseSimple GET requestsComplex 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

// ❌ 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

Real-World Exemple

Complete example from student project (step-17):

// album.service.ts
@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}`);
}
}
// album-list.component.ts
@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

  • ✅ Utiliser httpResource for 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

Pattern 1: Resource with Computed Filters

albumsResource = this.albumService.albumsResource;
albums = computed(() => this.albumsResource.value() ?? []);

// Filtered views
rockAlbums = computed(() =>
this.albums().filter(a => a.tags.includes('Rock'))
);

highlightedAlbums = computed(() =>
this.albums().filter(a => a.highlighted)
);

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

template: `
@if (albumsResource.error()) {
<div class="error-banner">
<p>Something went wrong!</p>
<button (click)="albumsResource.reload()">Try Again</button>
</div>
}
`


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