Leçon 5 / 15

Gérer les états de chargement et d’erreur

15 min React — données, effets et formulaires Gratuit

Objectif : Afficher un état de chargement, les données reçues ou un message d’erreur selon le résultat de la requête.

Quand vous ouvrez le tableau de bord, la liste ne peut pas toujours apparaître immédiatement : les données arrivent après une requête réseau. Sans indication, l’utilisateur peut croire que l’écran est vide ou bloqué. Vous allez afficher clairement le chargement, les dépenses reçues ou une erreur.

Dans la leçon précédente, vous avez déclenché la récupération au premier affichage et transmis les dépenses à ListeDepenses. Il manque encore deux informations : savoir si la réponse est en attente et savoir si la récupération a échoué. Ces informations décrivent le déroulement de la requête ; elles ne remplacent pas la collection de dépenses.

Distinguer les trois états de la requête

Le composant qui lance la requête conserve un booléen chargement et un message erreur, en plus de depenses. Au départ, le chargement est actif et aucun message d’erreur n’est présent. Si la requête réussit, vous enregistrez les données ; si elle échoue, vous enregistrez un message lisible. Dans les deux cas, le chargement se termine.

Une promesse fetch peut se résoudre même si le serveur répond avec un statut HTTP d’erreur, comme 404 ou 500. Vous devez donc examiner response.ok et lever une erreur lorsque la réponse n’est pas réussie. Les erreurs de réseau, elles, sont déjà signalées par le rejet de la promesse.

Faire remonter les erreurs du service

Le service est le point de passage de la requête HTTP. Il doit interrompre le traitement d’une réponse non réussie au lieu de tenter de lire son JSON comme s’il contenait des dépenses. En rejetant la promesse, il permet à App de traiter de la même manière les erreurs HTTP et les erreurs réseau.

src/services/apiDepenses.tsTypeScript
import type { Depense } from "../types";

export async function recupererDepenses(): Promise<Depense[]> {
  const response = await fetch("/api/depenses");

  if (!response.ok) {
    throw new Error(`La récupération a échoué (HTTP ${response.status}).`);
  }

  return (await response.json()) as Depense[];
}

Afficher l’état adapté dans l’interface

Dans App, vous initialisez les trois états, puis vous lancez la récupération dans l’effet déjà mis en place. La fonction asynchrone interne permet d’utiliser await sans rendre directement la fonction de l’effet asynchrone. Le bloc catch transforme une valeur d’erreur inconnue en message utilisable par l’interface, conformément au typage strict.

src/App.tsxTexte
import { useEffect, useState } from "react";
import { ListeDepenses } from "./composants/ListeDepenses";
import { recupererDepenses } from "./services/apiDepenses";
import type { Depense } from "./types";

export default function App() {
  const [depenses, setDepenses] = useState<Depense[]>([]);
  const [chargement, setChargement] = useState(true);
  const [erreur, setErreur] = useState<string | null>(null);

  useEffect(() => {
    async function charger() {
      try {
        const donnees = await recupererDepenses();
        setDepenses(donnees);
      } catch (cause: unknown) {
        setErreur(
          cause instanceof Error
            ? cause.message
            : "Une erreur inattendue est survenue."
        );
      } finally {
        setChargement(false);
      }
    }

    void charger();
  }, []);

  return (
    <div className="depenses">
      <header className="depenses__entete">
        <h1>Tableau de bord des dépenses</h1>
        <p>Suivez vos dépenses personnelles, vos catégories et votre budget mensuel.</p>
      </header>
      <main className="depenses__contenu">
        <section className="depenses__panneau" aria-labelledby="titre-liste">
          <h2 className="depenses__titre" id="titre-liste">Dépenses</h2>
          <div className="depenses__etat" role="status" aria-live="polite">
            {chargement && <p>Chargement des dépenses…</p>}
            {!chargement && erreur && <p role="alert">Impossible de charger les dépenses : {erreur}</p>}
            {!chargement && !erreur && depenses.length === 0 && <p>Aucune dépense à afficher.</p>}
          </div>
          {!chargement && !erreur && <ListeDepenses depenses={depenses} />}
        </section>
      </main>
    </div>
  );
}

Voici le composant de liste complet à cet état du projet. Il conserve le rendu typé et la clé stable déjà vus ; l’identifiant est utilisé comme clé, sans stocker de copie de la collection. Le composant parent décide quand la liste peut être affichée, tandis que le composant de liste se concentre sur les entrées reçues.

src/composants/ListeDepenses.tsxTexte
import type { Depense } from "../types";

type Props = {
  depenses: Depense[];
};

export function ListeDepenses({ depenses }: Props) {
  return (
    <ul className="depenses__liste">
      {depenses.map((depense) => (
        <li className="depenses__ligne" key={depense.id}>
          <span>{depense.libelle}</span>
          <span>{depense.categorie}</span>
          <time dateTime={depense.date}>{depense.date}</time>
          <span>{depense.montant.toFixed(2)} €</span>
        </li>
      ))}
    </ul>
  );
}

Le résultat dépend du serveur : pendant l’attente, la zone d’état annonce le chargement ; si le serveur renvoie une liste vide, un message d’absence de données apparaît ; si la requête échoue, la liste est masquée et le message d’erreur est annoncé. Si des dépenses sont reçues, elles sont affichées dans la liste. Les autres composants du tableau de bord restent inchangés dans cette étape.

À retenir

  • Conservez séparément la collection, l’indicateur de chargement et le message d’erreur.
  • Vérifiez response.ok : fetch ne rejette pas automatiquement les réponses HTTP 4xx ou 5xx.
  • Traitez les échecs dans catch et terminez le chargement dans finally.
  • Affichez un état compréhensible et ne rendez la liste que lorsque les données peuvent être présentées.

Dans la prochaine leçon, vous sécuriserez cet effet pour éviter qu’une requête encore active ne mette à jour un composant qui n’est plus affiché.

Envie d'aller plus loin ?

Les cours Enovatic sont gratuits et le resteront. Si vous cherchez un accompagnement encadré, nos formations professionnelles sont animées par les mêmes formateurs.