Retour au cours

frontend / react

Hooks personnalisés

Leçon 111 exercice

Explication

Ce que vous allez apprendre

  • Reconnaître la duplication de logique à état qui justifie l'extraction d'un hook
  • Écrire un hook personnalisé respectant la convention de nommage use...
  • Construire un hook useFetch réutilisable avec gestion de chargement et d'erreur
  • Implémenter un useDebounce pour limiter les appels réseau lors d'une saisie
  • Composer plusieurs hooks personnalisés entre eux comme des briques

Dans quel contexte ?

Trois composants différents — src/components/UserSearch.jsx, src/components/ProductSearch.jsx et src/components/TagSearch.jsx — implémentent chacun leur propre logique de "chargement/erreur/données" autour d'un fetch, avec de légères variations qui ont fini par diverger. Un bug corrigé dans l'un n'est jamais reporté dans les deux autres. Extraire un hook useFetch(url) commun élimine cette duplication et centralise le comportement à un seul endroit.

Étape 1 : des schémas de code qui reviennent

Au fil d'une application, certains schémas de code reviennent sans cesse : gérer un chargement autour d'un appel réseau, synchroniser une valeur avec le stockage local.

Étape 2 : le problème du copier-coller

Copier-coller ce genre de logique d'un composant à l'autre fonctionne, mais devient une source de bugs, un correctif appliqué dans un composant et oublié dans un autre.

Étape 3 : la solution, un hook personnalisé

Un hook personnalisé est simplement une fonction JavaScript ordinaire, à ceci près qu'elle utilise d'autres hooks à l'intérieur et que son nom commence obligatoirement par use.

Étape 4 : pourquoi ce nom est obligatoire

Cette convention de nommage permet à React et aux outils comme ESLint de savoir qu'ils doivent appliquer les règles des hooks à cette fonction, par exemple ne pas l'appeler dans une condition.

Étape 5 : ce que ça change concrètement

Un hook personnalisé permet d'extraire une logique à état entière dans une fonction réutilisable. Le composant qui l'utilise appelle juste useLocalStorage("theme", "clair") et récupère un résultat prêt à l'emploi.

Étape 6 : une synthèse de ce qui précède

Un hook personnalisé combine useState, useEffect, parfois useContext, pour offrir une brique réutilisable.

La règle pratique à retenir

Dès qu'une même logique à état se répète dans deux composants ou plus, c'est le signal qu'il est temps de l'extraire dans un hook custom dédié.

Bonne pratique

Nomme tes hooks selon ce qu'ils font, pas comment ils le font : useFetch, useDebounce, useLocalStorage sont plus clairs que useEffectWrapper ou useHelper. Un bon nom de hook se lit presque comme une phrase à l'appel : const { donnees, chargement } = useFetch(url);.

Logique répétée détectéeHook custom correspondant
Chargement/erreur autour d'un fetchuseFetch
Synchronisation avec localStorageuseLocalStorage
Attendre la fin de la frappe avant d'agiruseDebounce

Et ensuite ?

Une fois les hooks personnalisés maîtrisés, la prochaine étape aborde un état plus complexe, avec plusieurs champs liés entre eux, via useReducer.

Commandes & code

Hooks personnalisés

Extraire une logique à état réutilisable entre composants, sans dupliquer de code.

jsx
// Règle : un hook custom commence par "use" et peut appeler d'autres hooks à l'intérieur
function useLocalStorage(cle, valeurInitiale) {
    const [valeur, setValeur] = useState(() => {
        try {
            const stocke = window.localStorage.getItem(cle);
            return stocke ? JSON.parse(stocke) : valeurInitiale;
        } catch {
            return valeurInitiale;
        }
    });

    useEffect(() => {
        window.localStorage.setItem(cle, JSON.stringify(valeur));
    }, [cle, valeur]);

    return [valeur, setValeur]; // même API qu'un useState classique
}

function Preferences() {
    const [theme, setTheme] = useLocalStorage("theme", "clair");
    return (
        <button onClick={() => setTheme(theme === "clair" ? "sombre" : "clair")}>
            Thème : {theme}
        </button>
    );
}
jsx
// Hook de fetch réutilisable, avec états loading/error standardisés
function useFetch(url) {
    const [donnees, setDonnees] = useState(null);
    const [chargement, setChargement] = useState(true);
    const [erreur, setErreur] = useState(null);

    useEffect(() => {
        let annule = false;
        setChargement(true);
        setErreur(null);

        fetch(url)
            .then((res) => {
                if (!res.ok) throw new Error(`Erreur ${res.status}`);
                return res.json();
            })
            .then((data) => { if (!annule) setDonnees(data); })
            .catch((err) => { if (!annule) setErreur(err.message); })
            .finally(() => { if (!annule) setChargement(false); });

        return () => { annule = true; };
    }, [url]);

    return { donnees, chargement, erreur };
}

function ListeCours() {
    const { donnees, chargement, erreur } = useFetch("/api/cours");

    if (chargement) return <p>Chargement...</p>;
    if (erreur) return <p>Erreur : {erreur}</p>;
    return <ul>{donnees.map((c) => <li key={c.id}>{c.titre}</li>)}</ul>;
}
jsx
// Hook de debounce : très courant pour une recherche en temps réel
function useDebounce(valeur, delai) {
    const [valeurDebounced, setValeurDebounced] = useState(valeur);

    useEffect(() => {
        const id = setTimeout(() => setValeurDebounced(valeur), delai);
        return () => clearTimeout(id); // annule le timer précédent si "valeur" change trop vite
    }, [valeur, delai]);

    return valeurDebounced;
}

function Recherche() {
    const [terme, setTerme] = useState("");
    const termeDebounced = useDebounce(terme, 400);

    useEffect(() => {
        if (termeDebounced) {
            console.log("Recherche API :", termeDebounced); // déclenché 400ms après la dernière frappe
        }
    }, [termeDebounced]);

    return <input value={terme} onChange={(e) => setTerme(e.target.value)} />;
}
jsx
// Composer plusieurs hooks custom entre eux, comme des briques
function useUtilisateurConnecte() {
    const { utilisateur } = useAuth();
    const [preferences] = useLocalStorage(`prefs-${utilisateur?.id}`, {});
    return { utilisateur, preferences };
}

Résumé

  • Un hook custom commence obligatoirement par use : c'est ce qui active les règles des hooks du linter.
  • Il encapsule un state + des effets réutilisables, exposés via une API simple (souvent proche de useState).
  • Les hooks custom se composent librement entre eux, exactement comme les hooks natifs.
  • Extraire un hook custom dès qu'une logique à état (fetch, debounce, storage...) se répète dans 2+ composants.

Exercices pratiques

1 disponible
1

Mission : fusionner trois recherches dupliquées en un seul hook

Objectif : Extraire une logique fetch dupliquée dans un hook useFetch, et corriger un useDebounce mal câblé qui déclenche trop d'appels réseau.

Contexte

UserSearch.jsx, ProductSearch.jsx et TagSearch.jsx contiennent chacun leur propre copie de cette logique, avec de petites différences qui ont divergé au fil du temps : l'un oublie de gérer l'erreur, l'autre ne protège pas contre une réponse obsolète. Par ailleurs, ProductSearch appelle l'API directement dans onChange={(e) => { setTerme(e.target.value); fetch(/api/produits?q=${e.target.value}); }}, ce qui déclenche une requête réseau à CHAQUE frappe, saturant le serveur de test lors de la démo client.

Résoudre l’exercice →