Apprends à extraire de la logique stateful réutilisable dans des hooks personnalisés, à suivre les conventions de nommage, et à construire des hooks réels comme useLocalStorage, useToggle et useDebounce.
Au fur et à mesure que ton application React grandit, tu remarqueras que différents composants partagent souvent la même logique stateful. Par exemple, plusieurs composants pourraient avoir besoin de suivre si une modale est ouverte, de s'abonner aux événements de redimensionnement de la fenêtre, ou de debouncer la saisie utilisateur.
Avant les hooks, les seules façons de partager de la logique stateful étaient les Higher-Order Components (HOCs) et les Render Props — les deux menaient à des arbres de composants profondément imbriqués ("wrapper hell") et rendaient le code plus difficile à suivre.
Les hooks personnalisés résolvent ce problème élégamment. Un hook personnalisé est simplement une fonction JavaScript dont le nom commence par use et qui appelle d'autres hooks à l'intérieur. Tu extrais la logique partagée dans le hook personnalisé, et n'importe quel composant qui en a besoin peut l'appeler directement — pas de wrappers, pas d'imbrication.
// Avant : logique dupliquée dans deux composants
function ComponentA() {
const [isOpen, setIsOpen] = React.useState(false);
const toggle = () => setIsOpen(prev => !prev);
// ...
}
function ComponentB() {
const [isOpen, setIsOpen] = React.useState(false);
const toggle = () => setIsOpen(prev => !prev);
// ...
}
// Après : logique partagée dans un hook personnalisé
function useToggle(initial = false) {
const [value, setValue] = React.useState(initial);
const toggle = () => setValue(prev => !prev);
return [value, toggle];
}
function ComponentA() {
const [isOpen, toggle] = useToggle();
// ...
}Chaque composant qui appelle useToggle obtient sa propre copie indépendante du state. Les hooks personnalisés partagent la logique, pas le state.
Les hooks personnalisés doivent suivre une convention de nommage stricte et les mêmes règles que les hooks intégrés.
useChaque hook personnalisé doit commencer par le mot use, suivi d'une lettre majuscule. Ce n'est pas juste une convention — le linter de React s'appuie dessus pour faire respecter les règles des hooks.
// Bien — reconnu comme un hook
function useCounter() { ... }
function useLocalStorage(key) { ... }
function useWindowSize() { ... }
// Mal — PAS reconnu comme un hook
function getCounter() { ... } // le linter ne vérifiera pas les règles des hooks
function counter() { ... } // même problèmeLes hooks personnalisés doivent suivre les deux mêmes règles que tous les hooks :
// FAUX — appel de hook conditionnel
function useBadHook(condition) {
if (condition) {
const [value, setValue] = React.useState(0); // casse tout !
}
}
// CORRECT — appelle toujours le hook, utilise la condition pour le comportement
function useGoodHook(condition) {
const [value, setValue] = React.useState(0);
// utilise la condition dans le retour ou dans un effet
}Les hooks personnalisés peuvent appeler d'autres hooks personnalisés. C'est l'un des patterns les plus puissants — tu peux composer des hooks ensemble :
function useDebounce(value, delay) {
const [debounced, setDebounced] = React.useState(value);
React.useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
function useDebouncedSearch(query) {
const debouncedQuery = useDebounce(query, 300);
const [results, setResults] = React.useState([]);
// ... fetch basé sur debouncedQuery
return results;
}Construisons trois hooks personnalisés pratiques que tu utiliseras régulièrement dans de vrais projets.
Persiste le state dans localStorage pour qu'il survive aux rafraîchissements de page :
function useLocalStorage(key, initialValue) {
const [value, setValue] = React.useState(() => {
try {
const stored = localStorage.getItem(key);
return stored !== null ? JSON.parse(stored) : initialValue;
} catch {
return initialValue;
}
});
React.useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// Utilisation
function App() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
// theme persiste à travers les rafraîchissements de page !
}Note l'initialisateur paresseux passé à useState — la fonction fléchée () => { ... } s'exécute seulement une fois au premier rendu, évitant un appel localStorage.getItem à chaque rendu.
Un hook simple mais extrêmement courant pour un state booléen :
function useToggle(initial = false) {
const [value, setValue] = React.useState(initial);
const toggle = React.useCallback(() => {
setValue(prev => !prev);
}, []);
return [value, toggle];
}
// Utilisation
function Modal() {
const [isOpen, toggleOpen] = useToggle();
return (
<>
<button onClick={toggleOpen}>
{isOpen ? 'Fermer' : 'Ouvrir'}
</button>
{isOpen && <div className="modal">Contenu de la modale</div>}
</>
);
}Retarde la mise à jour d'une valeur jusqu'à ce que l'utilisateur arrête de la changer. Essentiel pour les champs de recherche :
function useDebounce(value, delay = 300) {
const [debounced, setDebounced] = React.useState(value);
React.useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
// Utilisation — recherche seulement après que l'utilisateur arrête de taper pendant 300ms
function SearchBar() {
const [query, setQuery] = React.useState('');
const debouncedQuery = useDebounce(query, 300);
React.useEffect(() => {
if (debouncedQuery) {
// fetch des résultats de recherche
}
}, [debouncedQuery]);
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}Les hooks personnalisés peuvent retourner n'importe quoi : une valeur unique, un tableau, ou un objet. La convention dépend du cas d'usage.
En suivant le pattern de useState, retourne un tableau quand les appelants voudront probablement renommer les valeurs :
function useToggle(initial) {
// ...
return [value, toggle]; // tableau
}
// L'appelant peut les nommer comme il veut :
const [isOpen, toggleOpen] = useToggle(false);
const [isDark, toggleDark] = useToggle(true);Quand tu retournes beaucoup de valeurs liées, utilise un objet pour que les appelants puissent déstructurer seulement ce dont ils ont besoin :
function useCounter(initial = 0) {
const [count, setCount] = React.useState(initial);
const increment = () => setCount(c => c + 1);
const decrement = () => setCount(c => c - 1);
const reset = () => setCount(initial);
return { count, increment, decrement, reset }; // objet
}
// L'appelant déstructure par nom :
const { count, increment, reset } = useCounter(10);Quand le hook calcule une seule valeur dérivée :
function useDebounce(value, delay) {
// ...
return debounced; // valeur unique
}
const debouncedSearch = useDebounce(searchTerm, 500);Choisis la forme de retour qui rend le hook le plus ergonomique pour ses appelants.
<div id="root"></div>
<script src="https://unpkg.com/react@19/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@19/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
// Custom hook: useCounter
function useCounter(initial = 0) {
const [count, setCount] = React.useState(initial);
const increment = () => setCount(c => c + 1);
const decrement = () => setCount(c => c - 1);
const reset = () => setCount(initial);
return { count, increment, decrement, reset };
}
// Custom hook: useToggle
function useToggle(initial = false) {
const [value, setValue] = React.useState(initial);
const toggle = () => setValue(prev => !prev);
return [value, toggle];
}
function App() {
const { count, increment, decrement, reset } = useCounter(0);
const [isVisible, toggleVisible] = useToggle(true);
return (
<div>
<h2>Démo useCounter</h2>
<p>Compte : {count}</p>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
<button onClick={reset}>Réinitialiser</button>
<h2>Démo useToggle</h2>
<button onClick={toggleVisible}>
{isVisible ? "Masquer" : "Afficher"} le Contenu
</button>
{isVisible && <p>Ce contenu est basculé !</p>}
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>Quelle est l'exigence principale de nommage pour un hook personnalisé ?