Extraire le chargement dans un hook personnalisé
Objectif : Centraliser la récupération des dépenses et les états de chargement et d’erreur dans un hook personnalisé typé, réutilisable depuis App.
Le chargement asynchrone oblige actuellement `App` à gérer à la fois la requête et ses états. Cette logique devient difficile à réutiliser et détourne le composant de son rôle dans le tableau de bord. Vous allez la regrouper dans un hook personnalisé, tout en laissant `App` propriétaire de la liste des dépenses et des filtres.
Les notions de `useEffect`, de gestion des erreurs et d’annulation ont déjà été abordées dans les leçons précédentes : vous les réutilisez ici, sans en changer le fonctionnement. Le hook ne crée pas une deuxième copie des dépenses ; il centralise l’opération qui les récupère et expose son résultat à `App`.
Définir le contrat du hook
Un hook personnalisé est une fonction qui utilise des hooks React pour encapsuler une logique réutilisable. Son nom commence par `use` : cette convention rend visible son rôle et permet aux outils React de vérifier les règles d’utilisation des hooks. Placez-le dans le fichier prévu, `src/hooks/useDepenses.ts`, et donnez-lui un retour explicite : la liste, l’état de chargement et l’erreur éventuelle.
import { useEffect, useState } from "react";
import { fetchDepenses } from "../services/apiDepenses";
import type { Depense } from "../types";
export interface EtatDepenses {
depenses: Depense[];
chargement: boolean;
erreur: string | null;
}
export function useDepenses(): EtatDepenses {
const [depenses, setDepenses] = useState<Depense[]>([]);
const [chargement, setChargement] = useState(true);
const [erreur, setErreur] = useState<string | null>(null);
useEffect(() => {
const controleur = new AbortController();
async function charger(): Promise<void> {
try {
const resultat = await fetchDepenses(controleur.signal);
setDepenses(resultat);
setErreur(null);
} catch (cause: unknown) {
if (cause instanceof Error && cause.name === "AbortError") {
return;
}
setErreur(
cause instanceof Error
? cause.message
: "Une erreur inconnue est survenue."
);
} finally {
if (!controleur.signal.aborted) {
setChargement(false);
}
}
}
void charger();
return () => controleur.abort();
}, []);
return { depenses, chargement, erreur };
}Le type `EtatDepenses` décrit précisément ce que le hook fournit à son appelant. `unknown` oblige à vérifier la nature de l’erreur avant d’en lire le message, ce qui reste compatible avec TypeScript strict. Le `finally` ne clôt pas un chargement annulé : le composant qui n’en a plus besoin ne doit pas recevoir de mise à jour tardive.
Consommer le hook dans App
Remplacez dans `App.tsx` l’effet et les états consacrés au chargement par un appel à `useDepenses`. La liste renvoyée devient l’état initial détenu par `App` ; les filtres et les valeurs calculées restent gérés comme dans la leçon précédente. Le hook ne reçoit pas les filtres et ne stocke ni liste filtrée ni récapitulatif : il a une seule responsabilité, charger les dépenses.
import { useState } from "react";
import { FormulaireDepense } from "./composants/FormulaireDepense";
import { FiltresDepenses } from "./composants/FiltresDepenses";
import { ListeDepenses } from "./composants/ListeDepenses";
import { RecapitulatifMensuel } from "./composants/RecapitulatifMensuel";
import { Entete } from "./composants/Entete";
import { useDepenses } from "./hooks/useDepenses";
import type { Depense } from "./types";
export default function App() {
const etatChargement = useDepenses();
const [depenses, setDepenses] = useState<Depense[]>(
etatChargement.depenses
);
const [mois, setMois] = useState("");
const [categorie, setCategorie] = useState("");
function ajouterDepense(depense: Depense): void {
setDepenses((precedentes) => [...precedentes, depense]);
}
const depensesDuMois = depenses.filter((depense) =>
mois ? depense.date.startsWith(mois) : true
);
const depensesFiltrees = depensesDuMois.filter((depense) =>
categorie ? depense.categorie === categorie : true
);
const totalMensuel = depensesDuMois.reduce(
(total, depense) => total + depense.montant,
0
);
return (
<div className="depenses">
<Entete />
<main className="depenses__contenu">
<section className="depenses__panneau">
<FormulaireDepense onAjouter={ajouterDepense} />
</section>
<section className="depenses__panneau">
<FiltresDepenses
mois={mois}
categorie={categorie}
onMoisChange={setMois}
onCategorieChange={setCategorie}
/>
</section>
<section className="depenses__panneau">
<ListeDepenses
depenses={depensesFiltrees}
chargement={etatChargement.chargement}
erreur={etatChargement.erreur}
/>
</section>
<section className="depenses__panneau">
<RecapitulatifMensuel
mois={mois}
total={totalMensuel}
nombre={depensesDuMois.length}
/>
</section>
</main>
</div>
);
}Cet extrait illustre l’appel du hook et la séparation des responsabilités ; gardez dans votre `App.tsx` les noms de props et le JSX exacts déjà établis dans votre projet. Important : pour que l’ajout conserve la liste chargée, l’état canonique ne doit pas être initialisé une seule fois depuis un état asynchrone. Dans le projet, faites plutôt alimenter l’état canonique par le résultat du chargement, ou faites évoluer le hook pour qu’il expose aussi l’action d’ajout ; ne conservez pas deux listes indépendantes.
À retenir
- Un hook personnalisé regroupe une logique React réutilisable et son nom commence par `use`.
- Déclarez un type de retour pour rendre explicites les données et états exposés.
- Le hook centralise la récupération et ses états ; les filtres et le récapitulatif restent dérivés dans `App`.
- Préservez l’annulation de la requête et veillez à ne pas créer une seconde liste canonique.
Dans la prochaine leçon, vous assemblerez les composants du tableau de bord et clarifierez leurs responsabilités dans l’arbre final.