Un React custom hook est la manière officielle de sortir une logique répétée de vos composants pour la placer dans une seule fonction réutilisable. Si vous remarquez que vous avez copié la même combinaison useState + useEffect dans trois composants distincts, c'est précisément le moment de l'extraire dans un hook. Dans ce guide, j'explique ce qu'est un custom hook, comment en écrire un et quand il est réellement rentable, à l'aide d'exemples concrets.
Ce qu'est un custom hook — et ce qu'il n'est pas
Un custom hook est une fonction JavaScript ordinaire dont le nom commence par use et qui peut appeler d'autres hooks en son sein (useState, useEffect, useRef, voire d'autres custom hooks). Il n'a aucune propriété magique ; React s'attend simplement à ce que toute fonction préfixée par use respecte les règles des hooks.
- Il partage la logique, pas l'état. Si deux composants utilisent le même hook, chacun obtient son propre état indépendant.
- Il renvoie des données, des fonctions ou une combinaison des deux — jamais du JSX.
- Ce n'est pas un composant ; il ne fait pas de rendu, il est seulement appelé depuis un composant (ou un autre hook).
La distinction la plus claire est la suivante : si votre raison de découper quelque chose est « réutiliser l'UI », extrayez un composant ; si c'est « réutiliser la logique », extrayez un hook.
Les deux règles : où peut-on les appeler
Les hooks, qu'ils soient personnalisés ou natifs, obéissent à deux règles, et les enfreindre rend votre application imprévisible :
- Appelez-les uniquement au niveau supérieur. N'appelez jamais un hook dans une boucle, une condition ou une fonction imbriquée. React peut ainsi compter sur le fait que les hooks s'exécutent dans le même ordre à chaque rendu.
- Appelez-les uniquement depuis des fonctions React. C'est-à-dire depuis des composants ou d'autres custom hooks. N'appelez pas un hook depuis une simple fonction utilitaire.
Pour faire respecter ces règles automatiquement, installez eslint-plugin-react-hooks ; il détecte les mauvais usages pendant que vous tapez encore.
Notre premier custom hook : useToggle
Commençons par quelque chose de simple dont vous avez pourtant constamment besoin. Menus déroulants, modales, « voir plus » — partout il faut inverser un booléen :
import { useState, useCallback } from 'react';
function useToggle(initial = false) {
const [value, setValue] = useState(initial);
const toggle = useCallback(() => setValue(v => !v), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return { value, toggle, setTrue, setFalse };
}
Son utilisation dans un composant est remarquablement propre :
function Sidebar() {
const { value: isOpen, toggle, setFalse } = useToggle();
return (
<>
<button onClick={toggle}>Menu</button>
{isOpen && <nav onMouseLeave={setFalse}>...</nav>}
</>
);
}
Si nous enveloppons les fonctions renvoyées dans useCallback, c'est pour éviter qu'elles ne soient recréées comme une nouvelle référence à chaque rendu ; vous pouvez ainsi les utiliser en toute sécurité comme dépendances de useEffect.
Un hook avec effets de bord : useLocalStorage
La vraie puissance apparaît dès que useEffect et les API du navigateur entrent en jeu. Un hook qui maintient une valeur synchronisée avec localStorage regroupe en un seul endroit une logique qui serait sinon répétée dans des dizaines de composants :
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const stored = window.localStorage.getItem(key);
return stored !== null ? JSON.parse(stored) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(value));
} catch {
// quota dépassé ou mode privé : ignorer silencieusement
}
}, [key, value]);
return [value, setValue];
}
Deux détails comptent ici. D'abord, nous avons passé une fonction à useState (un initialiseur paresseux) afin que la lecture de localStorage ne s'exécute qu'au premier rendu, et non à chaque rendu. Ensuite, nous avons enveloppé JSON.parse et l'accès à localStorage dans un try/catch, car des données corrompues ou un onglet privé refusant l'accès sont une source de bugs bien réelle en production.
Un hook de récupération de données avec nettoyage
L'erreur la plus fréquente avec la récupération asynchrone est de tenter de mettre à jour l'état après le démontage du composant. AbortController résout cela proprement :
import { useState, useEffect } from 'react';
function useFetch(url) {
const [state, setState] = useState({ data: null, error: null, loading: true });
useEffect(() => {
const controller = new AbortController();
setState({ data: null, error: null, loading: true });
fetch(url, { signal: controller.signal })
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(data => setState({ data, error: null, loading: false }))
.catch(error => {
if (error.name !== 'AbortError') {
setState({ data: null, error, loading: false });
}
});
return () => controller.abort();
}, [url]);
return state;
}
La fonction de nettoyage renvoyée par l'effet annule la requête précédente chaque fois que url change ou que le composant est démonté. Vous évitez ainsi à la fois les conditions de concurrence et le célèbre avertissement « Can't perform a React state update on an unmounted component ».
Quand extraire un hook, et quand attendre
L'abstraction prématurée est un véritable piège. Un ensemble de règles pratiques :
- Extrayez un hook dès que vous utilisez la même logique à au moins deux endroits (ou que vous êtes sûr de le faire).
- Si la logique d'effet et d'état dans un composant devient assez longue pour nuire à la lisibilité, l'extraire vaut la peine ne serait-ce que pour la testabilité.
- Écrire un
useFetchest instructif, mais si vous avez besoin de cache, de nouvelles tentatives et d'invalidation en production, une bibliothèque mature comme TanStack Query est généralement le meilleur choix. - Concevez la forme renvoyée par votre hook comme un contrat clair : décidez délibérément si vous renvoyez un tableau (positionnel, comme
useState) ou un objet (nommé, lorsqu'il y a plusieurs champs).
Questions fréquentes
Quelle est la différence entre un custom hook et une fonction normale ?
Un custom hook peut appeler d'autres hooks comme useState et useEffect et se branche sur le cycle de rendu de React ; une simple fonction utilitaire ne le peut pas. En pratique, la distinction est signalée par le préfixe use, et le linter applique ses règles en fonction de ce préfixe.
Si deux composants utilisent le même hook, partagent-ils l'état ?
Non. Le hook ne partage que la logique ; chaque composant obtient son propre état isolé. Si vous voulez réellement partager l'état, vous avez besoin de Context ou d'une solution de gestion d'état.
Comment tester les custom hooks ?
Avec l'utilitaire renderHook de React Testing Library, vous pouvez rendre le hook de façon isolée et vérifier ses valeurs renvoyées ainsi que les mises à jour déclenchées dans act. Si le hook contient une logique pure, vous pouvez aussi l'extraire dans une fonction séparée et la tester directement.
Vous voulez déplacer votre logique React répétée vers des hooks propres ? Travaillons ensemble sur votre architecture frontend et vos projets React — contactez-moi.