Chapitre 21 — Et après React ?
Les hooks React ne sont pas réservés à React : vous pouvez créer vos propres hooks . L’objectif est simple : réutiliser de la logique sans dupliquer du code, tout en gardant vos composants plus lisibles. Dans ce chapitre, on construit deux hooks très utiles : useLocalStorage et useFetch .
Plan du chapitre
1) Pourquoi créer un hook personnalisé ?
Un hook personnalisé sert à extraire une logique répétée.
Exemple : gérer une valeur persistée dans localStorage,
ou faire un fetch avec loading/erreur.
Un hook personnalisé n’ajoute pas de magie : c’est juste une fonction. La différence : il utilise (ou combine) des hooks React à l’intérieur.
2) Règles d’or des hooks
- Un hook personnalisé commence par
use:useLocalStorage - Les hooks doivent être appelés au top-level (pas dans un if / boucle)
- Un hook peut appeler d’autres hooks
- Un hook doit rester “prévisible” (pas de comportements cachés)
Pensez à un hook comme une “brique de logique” réutilisable.
3) Structure typique d’un hook
Un hook personnalisé :
- reçoit des paramètres
- utilise
useState/useEffect/useMemo… - retourne des valeurs et/ou des fonctions
export function useSomething(param) {
const [value, setValue] = useState(...);
useEffect(() => {
// ...
}, [param]);
return { value, setValue };
}
4) Hook 1 : useLocalStorage
Objectif : stocker une valeur dans le state et la persister dans localStorage,
pour qu’elle reste même après un refresh.
A) Créer le hook (ex : src/hooks/useLocalStorage.js)
import { useEffect, useState } from "react";
export function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch {
// en cas d'erreur (quota, mode privé, etc.), on ignore
}
}, [key, value]);
return [value, setValue];
}
B) Utilisation dans un composant
import { useLocalStorage } from "./hooks/useLocalStorage.js";
export default function Settings() {
const [theme, setTheme] = useLocalStorage("theme", "light");
return (
<div>
<p>Thème : {theme}</p>
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
Basculer
</button>
</div>
);
}
Remarquez l’astuce : useState(() => ...) (fonction) permet de lire le storage
une seule fois au montage, pas à chaque rendu.
5) Hook 2 : useFetch (chargement / erreur / données)
Objectif : encapsuler la logique “classique” d’un fetch : loading, error, data.
A) Version simple (débutant)
import { useEffect, useState } from "react";
export function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
if (!url) return;
let isMounted = true;
setLoading(true);
setError(null);
fetch(url)
.then((res) => {
if (!res.ok) throw new Error("Erreur HTTP : " + res.status);
return res.json();
})
.then((json) => {
if (isMounted) setData(json);
})
.catch((err) => {
if (isMounted) setError(err.message);
})
.finally(() => {
if (isMounted) setLoading(false);
});
return () => {
isMounted = false;
};
}, [url]);
return { data, loading, error };
}
B) Utilisation
import { useFetch } from "./hooks/useFetch.js";
export default function Users() {
const { data, loading, error } = useFetch("https://jsonplaceholder.typicode.com/users");
if (loading) return <p>Chargement...</p>
if (error) return <p>Erreur : {error}</p>
if (!data) return null;
return (
<ul>
{data.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
);
}
On isole la logique réseau dans le hook, et le composant reste clair : il affiche selon l’état.
6) Nettoyage (cleanup) et annulation
Pourquoi le cleanup ?
Si l’utilisateur change de page pendant un fetch, le composant peut être démonté.
Sans protection, on peut essayer de faire un setState sur un composant démonté.
Dans notre version débutant, on utilise isMounted.
Dans une version plus moderne, on peut utiliser AbortController (chapitre avancé).
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then(...)
.catch((err) => {
if (err.name === "AbortError") return;
// sinon vraie erreur
});
return () => controller.abort();
}, [url]);
7) Bonnes pratiques
- Un hook doit avoir un rôle clair (1 responsabilité).
- Retournez des données/fonctions simples, faciles à comprendre.
- Évitez les hooks “fourre-tout”.
- Placez vos hooks dans
src/hooks/. - Nommez clairement :
useAuth,useTheme,useFetch.
8) Résumé (à retenir)
- Un hook personnalisé factorise une logique et évite la duplication.
- Il commence par
useet peut appeler d’autres hooks. useLocalStorage: state + persistance.useFetch: data/loading/error + cleanup.
9) Exercice pratique
Objectif : créer un hook useCounter et un hook useLocalStorage utilisé dans une page.
A) Hook useCounter
- State :
count - Fonctions :
inc(),dec(),reset() - Retour :
{ count, inc, dec, reset }
B) Stocker la valeur du compteur en localStorage
Bonus : reliez le compteur à useLocalStorage pour garder la valeur après refresh.
Prochaine étape : Chapitre 22 — Appels API “propres” : séparer services/, gérer les erreurs, et préparer une app pour un backend.