Angular resource() API : données asynchrones réactives

Front-end angularforall.com
Angular Signals Resource Api Async Angular 19
Angular resource() API : données asynchrones réactives

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())
  });
}
L'idée centrale : 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);
  }
}
À noter : Le paramètre 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);
  }
}
Astuce débutant : Remarquez l'absence totale de 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 });
  }
}
Différence avec fetch natif : 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
  }
}
Quand choisir rxResource() ? Utilisez 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 fetch natif 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.';
  }
}
Skeleton loader : Au lieu d'un simple "Chargement...", utilisez les classes Bootstrap 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();
    }
  });
}
Annulation automatique : Quand 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"
  • abortSignal est passé aux appels fetch() natifs
  • Les ressources partagées sont dans des services, pas des composants
  • httpResource() est utilisé à la place de resource() + fetch quand des intercepteurs sont en place
  • Les ressources avec request undefined/null ont un comportement idle défini
Résumé : 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.

Partager