Maîtrisez resource(), httpResource() et rxResource() d'Angular 19 pour charger des données asynchrones avec les signaux : états, erreurs et rechargement.
Pourquoi resource() ? Le problème résolu
Avant Angular 19, charger des données asynchrones dans un composant était verbeux. Il fallait injecter HttpClient, s'abonner dans ngOnInit, gérer manuellement l'état de chargement, traiter les erreurs, et ne pas oublier de unsubscribe pour éviter les fuites mémoire.
Voici le code typique que vous écriviez avant :
// ❌ Ancienne approche — verbeux et source de bugs
@Component({
template: `
<div *ngIf="loading">Chargement...</div>
<div *ngIf="error">Erreur : {{ error }}</div>
<ul *ngIf="!loading && !error">
<li *ngFor="let produit of produits">{{ produit.nom }}</li>
</ul>
`
})
export class ProduitsComponent implements OnInit, OnDestroy {
produits: Produit[] = [];
loading = false;
error: string | null = null;
private destroy$ = new Subject<void>();
constructor(private http: HttpClient) {}
ngOnInit() {
this.loading = true;
this.http.get<Produit[]>('/api/produits')
.pipe(takeUntil(this.destroy$))
.subscribe({
next: data => { this.produits = data; this.loading = false; },
error: err => { this.error = err.message; this.loading = false; }
});
}
ngOnDestroy() {
this.destroy$.next(); // annuler les subscriptions
this.destroy$.complete();
}
}
Ce code a plusieurs problèmes : il est long, facile à oublier le unsubscribe, et difficile à faire réagir automatiquement si un paramètre change (ex. changer de catégorie de produits).
Avec resource() introduit dans Angular 19, tout ce code se réduit à quelques lignes :
// ✅ Nouvelle approche Angular 19 — concis et réactif
@Component({
template: `
@if (produits.isLoading()) { <p>Chargement...</p> }
@if (produits.error()) { <p>Erreur de chargement</p> }
@for (p of produits.value(); track p.id) {
<li>{{ p.nom }}</li>
}
`
})
export class ProduitsComponent {
// resource() gère loading, error et annulation automatiquement
produits = resource({
loader: () => fetch('/api/produits').then(r => r.json())
});
}
resource() transforme une opération asynchrone en signal Angular. Comme tous les signaux, il est réactif — si ses dépendances changent, il recharge automatiquement les données.
Angular 19 introduit trois variantes selon votre contexte :
| Fonction | Loader accepté | Cas d'usage |
|---|---|---|
resource() |
Promise (async/await) | fetch natif, IndexedDB, Web APIs |
httpResource() |
Configuration HTTP | Appels HTTP via HttpClient Angular |
rxResource() |
Observable RxJS | Code RxJS existant, WebSocket, SSE |
Les concepts fondamentaux
Avant d'écrire du code, il faut comprendre deux notions : les états d'une ressource et son lien avec les signaux.
Les états d'une ressource
Une ressource passe par différents états au cours de sa vie. Angular expose ces états via ResourceStatus :
| État | Signification | value() | isLoading() |
|---|---|---|---|
Idle |
Aucun chargement lancé | undefined |
false |
Loading |
Requête en cours | ancienne valeur ou undefined |
true |
Resolved |
Données disponibles | données fraîches | false |
Error |
Echec du chargement | ancienne valeur ou undefined |
false |
Local |
Valeur modifiée localement | valeur locale définie | false |
La réactivité automatique
La vraie puissance de resource() vient de sa réactivité avec les signaux. Si votre loader lit un signal, la ressource se recharge automatiquement quand ce signal change.
// Exemple : un signal "categorie" pilote le rechargement
export class CatalogueComponent {
// Signal qui représente la catégorie sélectionnée
categorieActive = signal('electronique');
// La ressource recharge automatiquement quand categorieActive change
produits = resource({
// request() est appelé à chaque changement de dépendance
request: () => ({ categorie: this.categorieActive() }),
loader: ({ request }) =>
fetch(`/api/produits?cat=${request.categorie}`).then(r => r.json())
});
changerCategorie(cat: string) {
// Juste mettre à jour le signal — resource() fait le reste
this.categorieActive.set(cat);
}
}
request est facultatif. Si votre loader n'a pas de paramètres variables, omettez-le. Utilisez-le quand les données dépendent de filtres, d'une page, d'un identifiant ou de tout signal changeant.
Premiers pas avec resource()
Commençons par un cas concret : afficher le profil d'un utilisateur. On va progressivement ajouter des fonctionnalités pour montrer toutes les capacités de resource().
Étape 1 — Installation et imports
resource() est disponible depuis @angular/core à partir d'Angular 19. Aucun package supplémentaire n'est nécessaire :
// Importer resource depuis @angular/core
import { Component, signal, resource } from '@angular/core';
// L'interface qui décrit nos données
interface Utilisateur {
id: number;
nom: string;
email: string;
avatar: string;
}
Étape 2 — Créer la ressource
@Component({
selector: 'app-profil-utilisateur',
standalone: true,
template: `
<!-- Afficher un spinner pendant le chargement -->
@if (utilisateur.isLoading()) {
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Chargement...</span>
</div>
}
<!-- Afficher l'erreur si le chargement a échoué -->
@if (utilisateur.error()) {
<div class="alert alert-danger">
Impossible de charger le profil.
</div>
}
<!-- Afficher les données quand elles sont disponibles -->
@if (utilisateur.value(); as profil) {
<div class="card">
<img [src]="profil.avatar" [alt]="profil.nom">
<h2>{{ profil.nom }}</h2>
<p>{{ profil.email }}</p>
</div>
}
`
})
export class ProfilUtilisateurComponent {
// Signal : identifiant de l'utilisateur à afficher
userId = signal(42);
// resource() crée un signal asynchrone lié à userId
utilisateur = resource<Utilisateur>({
// request() capture les dépendances réactives
request: () => this.userId(),
// loader reçoit la valeur du request et charge les données
loader: async ({ request: id }) => {
const reponse = await fetch(`/api/utilisateurs/${id}`);
if (!reponse.ok) {
// Lancer une erreur met la ressource en état "Error"
throw new Error(`Erreur HTTP : ${reponse.status}`);
}
return reponse.json() as Promise<Utilisateur>;
}
});
// Naviguer vers un autre utilisateur — le rechargement est automatique
voirUtilisateur(id: number) {
this.userId.set(id);
}
}
ngOnInit, subscribe ou unsubscribe. Angular gère le cycle de vie de la ressource automatiquement. Quand le composant est détruit, la requête en cours est annulée.
Étape 3 — Lire les propriétés de la ressource
Une ressource expose plusieurs signaux pour accéder à ses informations :
// Toutes les propriétés disponibles sur une ResourceRef
const res = resource({ loader: () => fetch('/api/data').then(r => r.json()) });
res.value() // Signal : la valeur chargée (undefined si pas encore disponible)
res.isLoading() // Signal boolean : true pendant le chargement
res.error() // Signal : l'erreur si le loader a lancé une exception
res.status() // Signal ResourceStatus : 'idle' | 'loading' | 'resolved' | 'error' | 'local'
res.reload() // Méthode : forcer un rechargement sans changer le request
res.set(valeur) // Méthode : définir une valeur locale (passe en état "local")
res.update(fn) // Méthode : modifier la valeur actuelle avec une fonction
httpResource() : HTTP simplifié
httpResource() est une surcouche de resource() optimisée pour les requêtes HTTP Angular. Elle utilise HttpClient en interne, ce qui signifie que vos intercepteurs, la gestion des tokens et le cache HTTP fonctionnent automatiquement.
Import et configuration
// httpResource est importé depuis @angular/common/http
import { Component, signal } from '@angular/core';
import { httpResource } from '@angular/common/http';
// Dans le module ou le bootstrap :
// provideHttpClient() doit être configuré (angular.json ou main.ts)
Exemple : liste d'articles de blog
interface Article {
id: number;
titre: string;
resume: string;
datePublication: string;
categorie: string;
}
@Component({
selector: 'app-liste-articles',
standalone: true,
template: `
<div class="row">
@for (article of articles.value() ?? []; track article.id) {
<div class="col-md-4 mb-3">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">{{ article.titre }}</h5>
<p class="card-text text-muted">{{ article.resume }}</p>
<span class="badge bg-primary">{{ article.categorie }}</span>
</div>
</div>
</div>
}
@if (articles.isLoading()) {
<p class="text-center">Chargement des articles...</p>
}
</div>
`
})
export class ListeArticlesComponent {
page = signal(1);
// httpResource() gère la configuration HTTP complète
articles = httpResource<Article[]>({
// L'URL peut réagir à des signaux
url: () => `/api/articles?page=${this.page()}&limite=9`,
// Options HTTP standards
method: 'GET',
headers: { 'Accept-Language': 'fr-FR' }
});
pageSuivante() {
this.page.update(p => p + 1);
// articles se recharge automatiquement avec la nouvelle page
}
}
Envoi de données avec httpResource()
httpResource() ne se limite pas au GET. Vous pouvez faire des POST, PUT, DELETE en spécifiant le method et le body :
// Créer un nouveau commentaire via POST
export class FormulaireCommentaireComponent {
nouveauCommentaire = signal({ auteur: '', texte: '', articleId: 0 });
soumission = httpResource<{ id: number; message: string }>({
url: '/api/commentaires',
method: 'POST',
// body réagit au signal nouveauCommentaire
body: () => this.nouveauCommentaire()
});
soumettre(auteur: string, texte: string, articleId: number) {
// Mettre à jour le signal déclenche la requête POST
this.nouveauCommentaire.set({ auteur, texte, articleId });
}
}
httpResource() passe automatiquement par les intercepteurs Angular. Si vous avez un intercepteur d'authentification qui ajoute un token Bearer, il s'applique sans configuration supplémentaire.
rxResource() : pour les Observables RxJS
Si vous avez du code existant basé sur RxJS, ou si vous travaillez avec des WebSockets, des Server-Sent Events, ou des Observables complexes, rxResource() est votre pont entre le monde RxJS et les signaux Angular.
// rxResource accepte un loader qui retourne un Observable
import { Component, signal } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { map, catchError, of } from 'rxjs';
interface Meteo {
ville: string;
temperature: number;
description: string;
icone: string;
}
@Component({
selector: 'app-meteo',
standalone: true,
template: `
@if (meteo.isLoading()) {
<p>Récupération de la météo...</p>
}
@if (meteo.value(); as donnees) {
<div class="d-flex align-items-center gap-3">
<img [src]="donnees.icone" [alt]="donnees.description">
<div>
<h3>{{ donnees.ville }}</h3>
<p class="display-6 fw-bold">{{ donnees.temperature }}°C</p>
<p>{{ donnees.description }}</p>
</div>
</div>
}
`
})
export class MeteoComponent {
villeRecherchee = signal('Paris');
private http = inject(HttpClient);
// rxResource() : le loader retourne un Observable
meteo = rxResource<Meteo>({
request: () => this.villeRecherchee(),
loader: ({ request: ville }) =>
this.http.get<any>(`/api/meteo?ville=${encodeURIComponent(ville)}`).pipe(
// Transformer la réponse brute en notre interface
map(data => ({
ville: data.name,
temperature: Math.round(data.main.temp),
description: data.weather[0].description,
icone: `https://openweathermap.org/img/wn/${data.weather[0].icon}.png`
}))
)
});
rechercherVille(ville: string) {
this.villeRecherchee.set(ville);
// rxResource recharge automatiquement avec la nouvelle ville
}
}
rxResource() quand vous avez des opérateurs RxJS complexes (switchMap, debounceTime, combineLatest) ou du code Observable existant à réutiliser. Pour les nouveaux projets sans RxJS existant, préférez resource() ou httpResource().
Comparaison des trois variantes
Choisir resource() quand :
- Vous utilisez
fetchnatif ou des Web APIs (IndexedDB, Cache API) - Votre projet n'utilise pas ou peu RxJS
- Vous voulez l'approche la plus simple
Choisir httpResource() quand :
- Vous faites des appels REST classiques
- Vous avez des intercepteurs Angular (auth, logs, retry)
- Vous voulez profiter du cache HTTP Angular
Choisir rxResource() quand :
- Vous avez du code RxJS existant à réutiliser
- Vous utilisez des opérateurs RxJS spécialisés
- Vous travaillez avec WebSocket ou SSE
Gérer les états : loading, error, idle
Une bonne expérience utilisateur nécessite de gérer correctement chaque état de la ressource. Voici un composant complet qui couvre tous les cas :
@Component({
selector: 'app-tableau-de-bord',
standalone: true,
template: `
<div class="container mt-4">
<!-- État IDLE : aucun chargement démarré -->
@if (commandes.status() === 'idle') {
<div class="text-center text-muted py-5">
<p>Sélectionnez une période pour afficher vos commandes.</p>
</div>
}
<!-- État LOADING : skeleton loader pour éviter le layout shift -->
@if (commandes.isLoading()) {
<div class="row g-3">
@for (i of [1,2,3,4,5,6]; track i) {
<div class="col-md-4">
<div class="card placeholder-glow">
<div class="card-body">
<h5 class="placeholder col-8"></h5>
<p class="placeholder col-12"></p>
<p class="placeholder col-6"></p>
</div>
</div>
</div>
}
</div>
}
<!-- État ERROR : message explicite avec bouton de retry -->
@if (commandes.error()) {
<div class="alert alert-danger d-flex align-items-center gap-3">
<i class="bi bi-exclamation-triangle-fill fs-4"></i>
<div>
<strong>Impossible de charger les commandes.</strong>
<p class="mb-1">{{ obtenirMessageErreur() }}</p>
<button class="btn btn-outline-danger btn-sm" (click)="commandes.reload()">
Réessayer
</button>
</div>
</div>
}
<!-- État RESOLVED / LOCAL : données disponibles -->
@if (commandes.value(); as liste) {
<div class="row g-3">
@for (cmd of liste; track cmd.id) {
<div class="col-md-4">
<div class="card border-0 shadow-sm">
<div class="card-body">
<span class="badge"
[class.bg-success]="cmd.statut === 'livree'"
[class.bg-warning]="cmd.statut === 'en-cours'"
[class.bg-secondary]="cmd.statut === 'annulee'">
{{ cmd.statut }}
</span>
<h6 class="mt-2">Commande #{{ cmd.id }}</h6>
<p class="text-muted mb-0">{{ cmd.montant | currency:'EUR' }}</p>
</div>
</div>
</div>
}
</div>
<p class="text-muted mt-3">{{ liste.length }} commandes trouvées</p>
}
</div>
`
})
export class TableauDeBordComponent {
periodeActive = signal<'semaine' | 'mois' | 'annee' | null>(null);
commandes = resource({
request: () => this.periodeActive(),
loader: async ({ request: periode }) => {
// Si aucune période sélectionnée, ne pas charger (état idle)
if (!periode) return undefined;
const reponse = await fetch(`/api/commandes?periode=${periode}`);
if (!reponse.ok) throw new Error(`Erreur serveur (${reponse.status})`);
return reponse.json();
}
});
// Extraire un message lisible depuis l'erreur
obtenirMessageErreur(): string {
const err = this.commandes.error();
if (err instanceof Error) return err.message;
return 'Une erreur inattendue est survenue.';
}
}
placeholder et placeholder-glow pour afficher une silhouette animée. L'utilisateur perçoit une interface plus réactive et professionnelle.
Rechargement, annulation et polling
Forcer un rechargement
Pour recharger les données sans changer les paramètres (ex. bouton "Actualiser"), utilisez reload() :
@Component({
template: `
<button class="btn btn-outline-primary" (click)="notifications.reload()"
[disabled]="notifications.isLoading()">
@if (notifications.isLoading()) {
<span class="spinner-border spinner-border-sm me-2"></span>
}
Actualiser
</button>
@for (notif of notifications.value() ?? []; track notif.id) {
<div class="alert alert-info">{{ notif.message }}</div>
}
`
})
export class NotificationsComponent {
notifications = resource({
loader: () => fetch('/api/notifications/non-lues').then(r => r.json())
});
}
Polling automatique
Pour rafraîchir automatiquement des données à intervalle régulier (ex. tableau de bord en temps réel), combinez resource() avec un signal de temps :
import { Component, signal, effect } from '@angular/core';
@Component({
selector: 'app-monitoring',
standalone: true,
template: `
<div class="d-flex justify-content-between align-items-center mb-3">
<h4>Monitoring serveurs</h4>
<small class="text-muted">Actualisation toutes les 30s</small>
</div>
@for (serveur of serveurs.value() ?? []; track serveur.nom) {
<div class="d-flex justify-content-between py-2 border-bottom">
<span>{{ serveur.nom }}</span>
<span [class.text-success]="serveur.statut === 'ok'"
[class.text-danger]="serveur.statut === 'ko'">
{{ serveur.statut === 'ok' ? '● Opérationnel' : '● Hors ligne' }}
</span>
</div>
}
`
})
export class MonitoringComponent {
// Signal de compteur incrémenté par le timer
private tickTimer = signal(0);
serveurs = resource({
// La dépendance au timer force un rechargement périodique
request: () => this.tickTimer(),
loader: () => fetch('/api/monitoring/serveurs').then(r => r.json())
});
constructor() {
// Incrémenter le timer toutes les 30 secondes
setInterval(() => {
this.tickTimer.update(t => t + 1);
}, 30_000);
}
}
Annulation des requêtes obsolètes
Un problème classique en asynchrone : l'utilisateur clique rapidement sur plusieurs éléments, et la dernière réponse reçue n'est pas forcément la dernière requête envoyée. resource() gère ce problème avec AbortSignal :
// resource() passe un AbortSignal au loader pour annuler les requêtes dépassées
export class RechercheComponent {
terme = signal('');
resultats = resource({
request: () => this.terme(),
loader: async ({ request: recherche, abortSignal }) => {
if (recherche.length < 2) return []; // Pas de recherche courte
const reponse = await fetch(
`/api/recherche?q=${encodeURIComponent(recherche)}`,
{ signal: abortSignal } // Passer l'AbortSignal au fetch
);
// Si la requête a été annulée, fetch lance une AbortError — ignorée par resource()
return reponse.json();
}
});
}
terme change avant que la requête précédente soit terminée, Angular appelle automatiquement abortSignal.abort(). La requête obsolète est annulée et seule la dernière réponse est utilisée. Zéro code de votre côté.
Bonnes pratiques et patterns avancés
Optimistic updates avec set()
Pour une UI réactive sans attendre la confirmation du serveur, mettez à jour localement d'abord, puis laissez la ressource se synchroniser :
// Pattern "optimistic update" : mise à jour locale immédiate
export class ListeTachesComponent {
taches = resource<Tache[]>({
loader: () => fetch('/api/taches').then(r => r.json())
});
toggleTache(id: number) {
// 1. Mise à jour immédiate côté client (optimistic)
this.taches.update(liste =>
liste?.map(t => t.id === id ? { ...t, fait: !t.fait } : t) ?? []
);
// 2. Envoyer la modification au serveur en arrière-plan
fetch(`/api/taches/${id}/toggle`, { method: 'PATCH' })
.then(r => {
if (!r.ok) {
// 3. En cas d'erreur : recharger depuis le serveur pour resynchroniser
this.taches.reload();
}
});
}
}
Typage strict avec TypeScript
Toujours typer vos ressources explicitement pour bénéficier de l'autocomplétion et de la détection d'erreurs à la compilation :
// Typer explicitement le générique de resource()
interface ProjetDetail {
id: number;
nom: string;
description: string;
membres: Array<{ id: number; nom: string; role: string }>;
dateCreation: Date;
}
// ✅ Typé : value() retourne ProjetDetail | undefined
const projet = resource<ProjetDetail>({
loader: async () => fetch('/api/projet/1').then(r => r.json())
});
// ❌ Non typé : value() retourne unknown — perte de l'autocomplétion
const projetSansType = resource({
loader: async () => fetch('/api/projet/1').then(r => r.json())
});
Extraire la logique dans un service
Pour les ressources partagées entre plusieurs composants, déclarez-les dans un service Angular :
// Service dédié aux données utilisateur
@Injectable({ providedIn: 'root' })
export class UtilisateurService {
// Signal de l'ID courant accessible depuis tout le service
utilisateurId = signal<number | null>(null);
// Ressource partageable via le service
profil = resource<ProfilUtilisateur | null>({
request: () => this.utilisateurId(),
loader: async ({ request: id }) => {
if (!id) return null;
const r = await fetch(`/api/utilisateurs/${id}`);
return r.json();
}
});
connecter(id: number) { this.utilisateurId.set(id); }
deconnecter() { this.utilisateurId.set(null); }
}
// Utilisation dans n'importe quel composant
@Component({ /* ... */ })
export class NavbarComponent {
// inject() dans les propriétés de classe — Angular 19+ moderne
private utilisateurService = inject(UtilisateurService);
profil = this.utilisateurService.profil; // réutilise la même ressource
}
Checklist avant de déployer
- Les ressources sont typées avec un générique TypeScript explicite
- Tous les états (loading, error, idle) ont un affichage dans le template
- Les erreurs affichent un message lisible + un bouton "Réessayer"
abortSignalest passé aux appelsfetch()natifs- Les ressources partagées sont dans des services, pas des composants
httpResource()est utilisé à la place deresource()+fetchquand des intercepteurs sont en place- Les ressources avec
requestundefined/null ont un comportement idle défini
resource(), httpResource() et rxResource() forment le triumvirat du chargement asynchrone réactif dans Angular 19+. Ils remplacent le pattern verbeux ngOnInit + subscribe + unsubscribe par une API déclarative, typée et automatiquement annulable. Commencez par httpResource() pour vos appels REST du quotidien.