frontend / react
Hooks personnalisés
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
useFetchréutilisable avec gestion de chargement et d'erreur - Implémenter un
useDebouncepour 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ée | Hook custom correspondant |
|---|---|
Chargement/erreur autour d'un fetch | useFetch |
Synchronisation avec localStorage | useLocalStorage |
| Attendre la fin de la frappe avant d'agir | useDebounce |
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.
// 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>
);
}// 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>;
}// 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)} />;
}// 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
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.