httpResource() Angular 19.2+ : requêtes HTTP signal-based, Resource<T>, status, isLoading, reload() et remplacement du pattern HttpClient.
Pourquoi httpResource() ? Le problème avec HttpClient
Depuis Angular 2, la façon standard d'effectuer des requêtes HTTP est d'utiliser HttpClient combiné à RxJS. Cette approche fonctionne très bien pour des cas complexes (retry, debounce, switchMap...), mais pour le cas le plus courant — charger des données au montage d'un composant — elle est souvent surdimensionnée.
Voici le pattern classique que tout développeur Angular connaît :
// ❌ Ancien pattern — HttpClient + RxJS + subscribe manuel
import { Component, OnInit, OnDestroy } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Subscription } from 'rxjs';
interface User {
id: number;
name: string;
email: string;
}
@Component({
selector: 'app-users',
standalone: true,
template: `
<div *ngIf="isLoading">Chargement...</div>
<div *ngIf="error">Erreur : {{ error }}</div>
<ul *ngIf="!isLoading">
<li *ngFor="let user of users">{{ user.name }}</li>
</ul>
`
})
export class UsersComponent implements OnInit, OnDestroy {
users: User[] = [];
isLoading = false;
error: string | null = null;
// Il faut stocker la subscription pour la désabonner manuellement
private sub!: Subscription;
constructor(private http: HttpClient) {}
ngOnInit() {
this.isLoading = true;
// subscribe() crée une subscription manuelle à gérer
this.sub = this.http.get<User[]>('/api/users').subscribe({
next: (data) => {
this.users = data;
this.isLoading = false;
},
error: (err) => {
this.error = err.message;
this.isLoading = false;
}
});
}
ngOnDestroy() {
// Oubliez ceci = memory leak garanti
this.sub.unsubscribe();
}
}
Ce code présente plusieurs problèmes :
- Boilerplate excessif : 3 propriétés d'état (
isLoading,error,users), une subscription à gérer, deux lifecycle hooks. - Risque de memory leak : oublier le
unsubscribe()dansngOnDestroyest une erreur fréquente chez les débutants. - Pas réactif aux signals : si un signal change (ex: l'ID de l'utilisateur sélectionné), il faut relancer la requête manuellement.
- Pas de synchronisation avec la Change Detection :
OnPushcomplique le tableau.
httpResource() est une API Angular 21 (signal-based) qui remplace ce pattern verbose par une syntaxe déclarative en une ligne, avec gestion automatique du cycle de vie, des états et de la réactivité.
Angular a introduit les Signals en version 16, puis les a progressivement généralisés. httpResource() est la concrétisation de cette vision pour les requêtes HTTP : une API orientée données qui expose l'état de la requête via des signals lisibles directement dans le template.
Premiers pas avec httpResource() : syntaxe de base
httpResource() est importé depuis @angular/common/http et s'utilise directement dans un composant ou un service. Il prend en premier argument l'URL de la requête (ou une fonction qui retourne l'URL) et retourne un objet Resource<T>.
Installation et prérequis :
# Vérifier la version d'Angular (nécessite 19.2+ pour httpResource stable)
ng version
# Mettre à jour si nécessaire
ng update @angular/core @angular/cli
Exemple minimal : charger une liste d'utilisateurs
// users.component.ts — syntaxe minimale avec httpResource()
import { Component } from '@angular/core';
import { httpResource } from '@angular/common/http';
interface User {
id: number;
name: string;
email: string;
}
@Component({
selector: 'app-users',
standalone: true,
template: `
<!-- isLoading() est un signal : pas de subscribe, pas de async pipe -->
@if (users.isLoading()) {
<p>Chargement en cours...</p>
}
<!-- value() retourne les données ou undefined si pas encore chargé -->
@if (users.value(); as data) {
<ul>
@for (user of data; track user.id) {
<li>{{ user.name }} — {{ user.email }}</li>
}
</ul>
}
<!-- error() retourne l'erreur si la requête a échoué -->
@if (users.error()) {
<p class="text-danger">Erreur de chargement</p>
}
`
})
export class UsersComponent {
// Une seule ligne remplace tout le boilerplate HttpClient + RxJS
// httpResource() retourne un Resource<User[]>
users = httpResource<User[]>('/api/users');
}
ngOnInit, plus de subscribe, plus de ngOnDestroy. Angular gère automatiquement le cycle de vie de la requête. La requête est lancée dès l'instanciation du composant.
Configurer les options de la requête :
// Passer des options HTTP comme avec HttpClient
users = httpResource<User[]>({
// URL de la requête
url: '/api/users',
// Méthode HTTP (GET par défaut)
method: 'GET',
// Headers personnalisés
headers: {
'Accept': 'application/json',
'X-Custom-Header': 'my-value'
},
// Paramètres de query string (?page=1&limit=20)
params: {
page: '1',
limit: '20'
}
});
Utiliser httpResource() dans un service injecté :
// user.service.ts — encapsuler httpResource dans un service Angular
import { Injectable } from '@angular/core';
import { httpResource } from '@angular/common/http';
interface User {
id: number;
name: string;
}
@Injectable({
providedIn: 'root'
})
export class UserService {
// Le Resource est créé une fois et partagé via le service
// Tous les composants injectant ce service liront le même état
readonly users = httpResource<User[]>('/api/users');
}
httpResource() doit être appelé dans un contexte d'injection (constructeur, champ de classe, ou runInInjectionContext). Il ne peut pas être appelé depuis une fonction utilitaire classique sans contexte Angular.
L'objet Resource<T> : value, status, isLoading, error
La valeur retournée par httpResource() est un objet Resource<T> qui expose plusieurs signals en lecture seule. Chaque signal se lit en l'appelant comme une fonction : resource.value(), resource.isLoading(), etc.
| Signal / Méthode | Type retourné | Description |
|---|---|---|
value() |
T | undefined |
Les données reçues, ou undefined avant la première réponse |
isLoading() |
boolean |
true pendant le chargement (requête en cours) |
status() |
ResourceStatus |
État détaillé : Idle, Loading, Resolved, Error, Reloading, Local |
error() |
unknown |
L'erreur levée lors d'un échec, ou undefined |
reload() |
boolean |
Méthode (pas un signal) : force la relance de la requête |
set(value) |
void |
Méthode : remplace localement la valeur sans requête HTTP (statut Local) |
update(fn) |
void |
Méthode : modifie localement la valeur courante via une fonction de transformation |
Lire chaque signal dans le template :
// product-detail.component.ts — utilisation complète des signals Resource<T>
import { Component, computed } from '@angular/core';
import { httpResource, ResourceStatus } from '@angular/common/http';
interface Product {
id: number;
name: string;
price: number;
stock: number;
}
@Component({
selector: 'app-product-detail',
standalone: true,
template: `
<!-- Afficher l'état détaillé via status() -->
<p>État : {{ statusLabel() }}</p>
<!-- isLoading() : indicateur simple de chargement -->
@if (product.isLoading()) {
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Chargement...</span>
</div>
}
<!-- value() : données disponibles -->
@if (product.value(); as p) {
<h2>{{ p.name }}</h2>
<p>Prix : {{ p.price | currency:'EUR' }}</p>
<p>Stock : {{ p.stock }} unités</p>
}
<!-- error() : message d'erreur typé -->
@if (product.error()) {
<div class="alert alert-danger">
Impossible de charger le produit.
</div>
}
<!-- Bouton de rechargement manuel -->
<button (click)="product.reload()" [disabled]="product.isLoading()">
Actualiser
</button>
`
})
export class ProductDetailComponent {
// Charger un produit depuis l'API
product = httpResource<Product>('/api/products/42');
// Computed signal : transformer le statut en libellé lisible
statusLabel = computed(() => {
switch (this.product.status()) {
case ResourceStatus.Idle: return 'En attente';
case ResourceStatus.Loading: return 'Chargement...';
case ResourceStatus.Resolved: return 'Chargé';
case ResourceStatus.Error: return 'Erreur';
case ResourceStatus.Reloading: return 'Rechargement...';
case ResourceStatus.Local: return 'Modifié localement';
default: return 'Inconnu';
}
});
}
Utiliser set() pour une mise à jour optimiste :
// Mise à jour optimiste : modifier la valeur locale avant confirmation serveur
// Utile pour une UX fluide (pas d'attente du rechargement HTTP)
updateStock(newStock: number): void {
// Modifier immédiatement la valeur affichée (statut passe à "Local")
this.product.update((current) => {
// Si pas de données chargées, ne rien faire
if (!current) return current;
// Retourner une nouvelle version avec le stock mis à jour
return { ...current, stock: newStock };
});
// Envoyer la mise à jour au serveur en parallèle
this.http.patch('/api/products/42', { stock: newStock }).subscribe({
error: () => {
// En cas d'erreur serveur, recharger les données réelles
this.product.reload();
}
});
}
set() ou update() sont appelés, le statut passe à ResourceStatus.Local. La valeur locale est affichée jusqu'au prochain reload() ou changement de paramètre.
Requêtes réactives avec des paramètres Signal
C'est là que httpResource() montre sa vraie puissance. En passant une fonction comme premier argument (au lieu d'une URL statique), la requête se relance automatiquement chaque fois qu'un signal lu dans cette fonction change. C'est la réactivité "signal-based" en action.
Exemple : filtrage de produits en temps réel
// product-list.component.ts — requêtes réactives avec signal()
import { Component, signal, computed } from '@angular/core';
import { httpResource } from '@angular/common/http';
import { FormsModule } from '@angular/forms';
interface Product {
id: number;
name: string;
category: string;
price: number;
}
@Component({
selector: 'app-product-list',
standalone: true,
imports: [FormsModule],
template: `
<!-- Champ de recherche lié au signal -->
<input
type="text"
[(ngModel)]="searchQuery"
placeholder="Rechercher un produit..."
class="form-control mb-3"
/>
<!-- Sélecteur de catégorie -->
<select [(ngModel)]="selectedCategory" class="form-select mb-3">
<option value="">Toutes les catégories</option>
<option value="electronics">Électronique</option>
<option value="clothing">Vêtements</option>
</select>
@if (products.isLoading()) {
<p>Recherche en cours...</p>
}
@for (product of products.value() ?? []; track product.id) {
<div class="card mb-2">
<div class="card-body">
<h5>{{ product.name }}</h5>
<p>{{ product.price }}€ — {{ product.category }}</p>
</div>
</div>
}
`
})
export class ProductListComponent {
// Signals liés aux inputs utilisateur
searchQuery = signal('');
selectedCategory = signal('');
// httpResource avec une fonction : la requête se relance
// automatiquement quand searchQuery() ou selectedCategory() changent
products = httpResource<Product[]>(() => {
// Lire les signals déclenche la réactivité automatiquement
const query = this.searchQuery();
const category = this.selectedCategory();
// Construire l'URL avec les paramètres dynamiques
const params = new URLSearchParams();
if (query) params.set('q', query);
if (category) params.set('category', category);
// Retourner l'URL complète — Angular relancera la requête à chaque changement
return `/api/products?${params.toString()}`;
});
}
Désactiver la requête conditionnellement avec undefined :
// Requête conditionnelle : ne se déclenche que si userId est défini
import { Component, input } from '@angular/core';
import { httpResource } from '@angular/common/http';
interface UserProfile {
id: number;
name: string;
avatar: string;
}
@Component({
selector: 'app-user-profile',
standalone: true,
template: `
@if (profile.value(); as user) {
<img [src]="user.avatar" [alt]="user.name" />
<h3>{{ user.name }}</h3>
}
`
})
export class UserProfileComponent {
// Input signal : reçu du composant parent
userId = input<number | null>(null);
// La requête ne se lance QUE si userId est non-null
// Retourner undefined depuis la fonction suspend la requête
profile = httpResource<UserProfile>(() => {
const id = this.userId();
// Si pas d'ID, retourner undefined = requête suspendue (statut Idle)
if (!id) return undefined;
// Sinon, construire l'URL avec l'ID
return `/api/users/${id}`;
});
}
Débounce des requêtes avec un signal calculé :
// Éviter de spammer l'API à chaque frappe clavier
// Utiliser un signal debounced (pattern custom)
import { Component, signal, computed, effect } from '@angular/core';
import { httpResource } from '@angular/common/http';
@Component({ selector: 'app-search', standalone: true, template: '' })
export class SearchComponent {
// Signal brut mis à jour à chaque frappe
rawQuery = signal('');
// Signal debounced : mis à jour après 300ms de pause
debouncedQuery = signal('');
constructor() {
let timeout: ReturnType<typeof setTimeout>;
// effect() surveille rawQuery et met à jour debouncedQuery avec délai
effect(() => {
const query = this.rawQuery(); // tracker la dépendance
clearTimeout(timeout);
timeout = setTimeout(() => {
// Après 300ms sans frappe, mettre à jour le signal debounced
this.debouncedQuery.set(query);
}, 300);
});
}
// La requête HTTP utilise le signal debounced, pas le brut
results = httpResource<string[]>(() => {
const q = this.debouncedQuery();
if (q.length < 2) return undefined; // Ne pas chercher si moins de 2 caractères
return `/api/search?q=${encodeURIComponent(q)}`;
});
}
reload() et mutations : quand forcer le rechargement
reload() est la méthode permettant de forcer une nouvelle requête HTTP sans changer les paramètres. C'est l'équivalent d'un bouton "actualiser". Elle est indispensable après une mutation (POST/PUT/DELETE) pour resynchroniser les données affichées.
Pattern CRUD complet avec reload() après mutation :
// todo-list.component.ts — CRUD complet avec httpResource + reload()
import { Component, signal, inject } from '@angular/core';
import { httpResource, HttpClient } from '@angular/common/http';
import { FormsModule } from '@angular/forms';
interface Todo {
id: number;
title: string;
completed: boolean;
}
@Component({
selector: 'app-todo-list',
standalone: true,
imports: [FormsModule],
template: `
<h2>Mes tâches</h2>
<!-- Formulaire d'ajout -->
<form (ngSubmit)="addTodo()" class="d-flex gap-2 mb-3">
<input
[(ngModel)]="newTitle"
name="title"
placeholder="Nouvelle tâche..."
class="form-control"
required
/>
<button type="submit" class="btn btn-primary" [disabled]="isSaving()">
{{ isSaving() ? 'Ajout...' : 'Ajouter' }}
</button>
</form>
<!-- Liste des tâches -->
@if (todos.isLoading()) {
<div class="spinner-border" role="status"></div>
}
@for (todo of todos.value() ?? []; track todo.id) {
<div class="d-flex align-items-center gap-2 mb-2">
<input
type="checkbox"
[checked]="todo.completed"
(change)="toggleTodo(todo)"
class="form-check-input"
/>
<span [class.text-decoration-line-through]="todo.completed">
{{ todo.title }}
</span>
<button (click)="deleteTodo(todo.id)" class="btn btn-sm btn-danger ms-auto">
Supprimer
</button>
</div>
}
`
})
export class TodoListComponent {
// HttpClient encore utile pour les mutations (POST/PUT/DELETE)
private http = inject(HttpClient);
newTitle = signal('');
isSaving = signal(false);
// httpResource gère le GET et l'état de la liste
todos = httpResource<Todo[]>('/api/todos');
addTodo(): void {
const title = this.newTitle().trim();
if (!title) return;
this.isSaving.set(true);
// POST avec HttpClient pour créer la tâche
this.http.post<Todo>('/api/todos', { title, completed: false }).subscribe({
next: () => {
this.newTitle.set(''); // Vider le champ
this.isSaving.set(false);
// reload() relance le GET pour afficher la nouvelle tâche
this.todos.reload();
},
error: () => this.isSaving.set(false)
});
}
toggleTodo(todo: Todo): void {
// PATCH pour mettre à jour le statut
this.http.patch(`/api/todos/${todo.id}`, { completed: !todo.completed }).subscribe({
next: () => {
// Mise à jour optimiste locale via update() pour UX immédiate
this.todos.update((list) =>
list?.map(t => t.id === todo.id ? { ...t, completed: !t.completed } : t)
);
// Pas besoin de reload() car on a mis à jour localement
}
});
}
deleteTodo(id: number): void {
this.http.delete(`/api/todos/${id}`).subscribe({
next: () => {
// reload() après suppression pour resynchroniser la liste
this.todos.reload();
}
});
}
}
reload(), le statut passe à ResourceStatus.Reloading (et non Loading). La valeur précédente reste disponible via value() pendant le rechargement — parfait pour éviter le flash blanc.
Recharger périodiquement (polling) :
// Polling : recharger les données toutes les 30 secondes
import { Component, OnInit, OnDestroy, inject } from '@angular/core';
import { httpResource } from '@angular/common/http';
interface ServerStatus {
status: 'ok' | 'degraded' | 'down';
latency: number;
}
@Component({
selector: 'app-server-status',
standalone: true,
template: `
@if (status.value(); as s) {
<span [class]="'badge bg-' + badgeColor(s.status)">
{{ s.status.toUpperCase() }} — {{ s.latency }}ms
</span>
}
<!-- Afficher l'heure du dernier rafraîchissement -->
<small class="text-muted ms-2">Mis à jour : {{ lastUpdate() }}</small>
`
})
export class ServerStatusComponent implements OnInit, OnDestroy {
status = httpResource<ServerStatus>('/api/health');
// Stocker l'ID de l'intervalle pour pouvoir le nettoyer
private pollInterval!: ReturnType<typeof setInterval>;
private _lastUpdate = '';
ngOnInit(): void {
// Lancer le polling toutes les 30 secondes
this.pollInterval = setInterval(() => {
this.status.reload();
this._lastUpdate = new Date().toLocaleTimeString('fr-FR');
}, 30_000);
}
ngOnDestroy(): void {
// Arrêter le polling quand le composant est détruit
clearInterval(this.pollInterval);
}
lastUpdate(): string {
return this._lastUpdate || 'Jamais';
}
badgeColor(s: string): string {
return s === 'ok' ? 'success' : s === 'degraded' ? 'warning' : 'danger';
}
}
httpResource() vs HttpClient + RxJS : comparatif
httpResource() ne remplace pas HttpClient dans tous les cas. Les deux APIs coexistent et se complètent. Voici un guide clair pour savoir quand choisir l'une ou l'autre.
| Critère | httpResource() | HttpClient + RxJS |
|---|---|---|
| Lecture de données (GET) | Recommandé | Fonctionne, plus verbeux |
| Mutations (POST/PUT/DELETE) | Non supporté directement | Recommandé |
| Requêtes réactives (signal-driven) | Natif et automatique | Manuel (switchMap, combineLatest) |
| Gestion d'état isLoading/error | Intégrée (signals) | Manuelle (3+ propriétés) |
| Opérateurs complexes (retry, debounce) | Limité | Puissant (tout l'écosystème RxJS) |
| Requêtes parallèles (forkJoin) | Non supporté directement | Recommandé |
| Mise en cache côté client | À venir (ResourceCache) | Manuel (shareReplay) |
| Courbe d'apprentissage | Faible (débutants) | Élevée (RxJS obligatoire) |
| Tests unitaires | Simple (mock signal) | Complexe (marble testing) |
Arbre de décision — choisir la bonne API :
- Vous chargez des données en GET pour affichage dans un template → httpResource()
- La requête dépend d'un ou plusieurs signals → httpResource()
- Vous faites un POST/PUT/DELETE → HttpClient + subscribe()
- Vous enchaînez plusieurs appels API avec switchMap → HttpClient + RxJS
- Vous avez besoin de retry avec backoff exponentiel → HttpClient + RxJS
- Vous lancez plusieurs requêtes en parallèle avec forkJoin → HttpClient + RxJS
httpResource() et HttpClient coexistent dans le même composant. httpResource() gère les lectures réactives, HttpClient gère les mutations. C'est le pattern recommandé par l'équipe Angular.
Migrer un composant existant : avant / après :
// AVANT — HttpClient + RxJS (25 lignes)
@Component({ selector: 'app-articles', standalone: true, template: '' })
export class ArticlesComponentBefore implements OnInit, OnDestroy {
articles: Article[] = [];
isLoading = false;
error: string | null = null;
private sub!: Subscription;
constructor(private http: HttpClient) {}
ngOnInit(): void {
this.isLoading = true;
this.sub = this.http.get<Article[]>('/api/articles').subscribe({
next: (data) => { this.articles = data; this.isLoading = false; },
error: (e) => { this.error = e.message; this.isLoading = false; }
});
}
ngOnDestroy(): void { this.sub.unsubscribe(); }
}
// APRÈS — httpResource() (5 lignes)
@Component({ selector: 'app-articles', standalone: true, template: '' })
export class ArticlesComponentAfter {
// Remplace les 25 lignes précédentes par une seule
// isLoading, error, les données et le cycle de vie sont gérés automatiquement
articles = httpResource<Article[]>('/api/articles');
}
Gestion des erreurs et états de chargement dans le template
Une gestion robuste des erreurs est essentielle pour une bonne expérience utilisateur. httpResource() expose le signal error() qui contient l'erreur brute. Il faut la typer correctement pour afficher des messages pertinents.
Typer et afficher les erreurs HTTP :
// error-handling.component.ts — gestion complète des erreurs
import { Component, computed } from '@angular/core';
import { httpResource } from '@angular/common/http';
import { HttpErrorResponse } from '@angular/common/http';
interface Article {
id: number;
title: string;
content: string;
}
@Component({
selector: 'app-article-detail',
standalone: true,
template: `
<!-- Squelette de chargement (skeleton screen) -->
@if (article.isLoading()) {
<div class="skeleton-wrapper">
<div class="skeleton-title"></div>
<div class="skeleton-text"></div>
<div class="skeleton-text short"></div>
</div>
}
<!-- Affichage des données -->
@if (article.value(); as a) {
<article>
<h1>{{ a.title }}</h1>
<p>{{ a.content }}</p>
</article>
}
<!-- Message d'erreur contextualisé -->
@if (errorMessage()) {
<div class="alert alert-danger d-flex align-items-center gap-2" role="alert">
<i class="bi bi-exclamation-triangle-fill"></i>
<div>
{{ errorMessage() }}
<button class="btn btn-sm btn-outline-danger ms-2" (click)="article.reload()">
Réessayer
</button>
</div>
</div>
}
`
})
export class ArticleDetailComponent {
// Charger l'article avec l'ID 1
article = httpResource<Article>('/api/articles/1');
// Signal computed : transformer l'erreur brute en message lisible
errorMessage = computed(() => {
const err = this.article.error();
if (!err) return null; // Pas d'erreur
// Typer l'erreur comme HttpErrorResponse pour accéder au status
if (err instanceof HttpErrorResponse) {
switch (err.status) {
case 0: return 'Connexion impossible. Vérifiez votre réseau.';
case 401: return 'Vous devez être connecté pour accéder à cet article.';
case 403: return 'Accès refusé. Vous n\'avez pas les permissions nécessaires.';
case 404: return 'Article introuvable.';
case 500: return 'Erreur serveur. Veuillez réessayer dans quelques instants.';
default: return `Erreur ${err.status} : ${err.statusText}`;
}
}
// Erreur réseau ou autre
return 'Une erreur inattendue est survenue.';
});
}
Template de chargement avec état "Reloading" distinct :
// Distinguer le premier chargement du rechargement
// pour une UX plus fine
import { Component, computed } from '@angular/core';
import { httpResource, ResourceStatus } from '@angular/common/http';
@Component({
selector: 'app-feed',
standalone: true,
template: `
<!-- Barre de progression fine en haut — visible seulement pendant reload -->
@if (isReloading()) {
<div class="progress" style="height: 3px; margin-bottom: 1rem;">
<div class="progress-bar progress-bar-animated progress-bar-striped w-100"></div>
</div>
}
<!-- Spinner pleine page seulement au premier chargement -->
@if (isFirstLoad()) {
<div class="d-flex justify-content-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Chargement...</span>
</div>
</div>
} @else {
<!-- Afficher les données même pendant un reload (évite le flash blanc) -->
@for (item of feed.value() ?? []; track item.id) {
<div class="card mb-3">
<div class="card-body">{{ item.title }}</div>
</div>
}
}
<button (click)="feed.reload()" [disabled]="feed.isLoading()" class="btn btn-secondary">
Actualiser le fil
</button>
`
})
export class FeedComponent {
feed = httpResource<{ id: number; title: string }[]>('/api/feed');
// Vrai uniquement pendant le tout premier chargement (pas de valeur encore)
isFirstLoad = computed(
() => this.feed.status() === ResourceStatus.Loading && !this.feed.value()
);
// Vrai lors des rechargements suivants (valeur déjà présente)
isReloading = computed(
() => this.feed.status() === ResourceStatus.Reloading
);
}
Checklist de robustesse
- Toujours afficher un état de chargement (
isLoading()oustatus()) - Toujours afficher un message d'erreur contextualisé (
error()) - Toujours proposer un bouton "Réessayer" appelant
reload() - Utiliser
?? []ou?? nullsurvalue()pour éviter les erreurs de template - Distinguer
Loading(premier chargement) etReloadingpour une UX fine - Typer l'erreur en
HttpErrorResponsepour des messages précis - Ne jamais bloquer l'UI pendant un rechargement : conserver la valeur affichée
- Utiliser
computed()pour transformer les signaux dérivés (labels, couleurs…) - Retourner
undefineddepuis la fonction URL pour suspendre la requête si nécessaire - Tester les états
isLoading,erroretvaluedans les specs Jasmine/Jest
Tester un composant utilisant httpResource() :
// article-detail.component.spec.ts — tester les états du resource
import { TestBed } from '@angular/core/testing';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { provideHttpClient } from '@angular/common/http';
import { ArticleDetailComponent } from './article-detail.component';
describe('ArticleDetailComponent', () => {
let httpMock: HttpTestingController;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ArticleDetailComponent],
providers: [
// Fournir le client HTTP et son intercepteur de test
provideHttpClient(),
provideHttpClientTesting()
]
}).compileComponents();
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
// Vérifier qu'aucune requête inattendue n'est en attente
httpMock.verify();
});
it('doit afficher les données après chargement', () => {
const fixture = TestBed.createComponent(ArticleDetailComponent);
const component = fixture.componentInstance;
// isLoading doit être true immédiatement
expect(component.article.isLoading()).toBeTrue();
// Simuler la réponse API
const mockArticle = { id: 1, title: 'Test', content: 'Contenu...' };
httpMock.expectOne('/api/articles/1').flush(mockArticle);
fixture.detectChanges(); // Déclencher la détection de changements
// Les données doivent maintenant être disponibles
expect(component.article.value()).toEqual(mockArticle);
expect(component.article.isLoading()).toBeFalse();
expect(component.article.error()).toBeUndefined();
});
it('doit exposer une erreur 404 via error()', () => {
const fixture = TestBed.createComponent(ArticleDetailComponent);
const component = fixture.componentInstance;
// Simuler une erreur 404
httpMock.expectOne('/api/articles/1').flush(
{ message: 'Not Found' },
{ status: 404, statusText: 'Not Found' }
);
fixture.detectChanges();
// L'erreur doit être exposée par le signal error()
expect(component.article.error()).toBeTruthy();
expect(component.article.value()).toBeUndefined();
// Le message computed doit retourner 'Article introuvable.'
expect(component.errorMessage()).toBe('Article introuvable.');
});
});
Conclusion : l'avenir HTTP signal-based dans Angular
httpResource() représente une évolution majeure dans la façon de gérer les requêtes HTTP dans Angular. En remplaçant le pattern verbeux HttpClient + subscribe + ngOnDestroy par une API déclarative signal-based, Angular réduit considérablement le boilerplate tout en améliorant la lisibilité et la robustesse du code.
La puissance réelle de httpResource() réside dans sa réactivité automatique : en passant une fonction qui lit des signals, les requêtes se relancent naturellement à chaque changement de paramètre, sans aucun code de gestion manuelle. C'est particulièrement précieux pour les interfaces de filtrage, de recherche et de pagination.
La cohabitation avec HttpClient reste nécessaire pour les mutations (POST, PUT, DELETE) et les cas avancés nécessitant les opérateurs RxJS. La stratégie recommandée est simple : httpResource() pour les lectures, HttpClient pour les écritures.
httpResource() pour tout nouveau composant Angular qui charge des données. Migrez progressivement les composants existants en commençant par les plus simples (GET sans side-effects). La réduction de boilerplate et la clarté du code en valent largement l'effort.