Avancé30 min de lecture

Hooks Personnalisés

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.

Pourquoi des Hooks Personnalisés ?

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.

jsx
// 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.

La Convention de Nommage et les Règles

Les hooks personnalisés doivent suivre une convention de nommage stricte et les mêmes règles que les hooks intégrés.

Le préfixe use

Chaque 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.

javascript
// 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ème

Règles des Hooks (récapitulatif)

Les hooks personnalisés doivent suivre les deux mêmes règles que tous les hooks :

  1. Appelle les hooks uniquement au niveau supérieur. N'appelle pas de hooks à l'intérieur de boucles, de conditions, ou de fonctions imbriquées. React s'appuie sur le fait que l'ordre des appels de hooks soit le même à chaque rendu.
jsx
// 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
}
  1. Appelle les hooks uniquement depuis des composants fonction React ou d'autres hooks personnalisés. N'appelle pas de hooks depuis des fonctions JavaScript régulières, des composants classe, ou des gestionnaires d'événements en dehors des composants.

Hooks appelant des hooks

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 :

jsx
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;
}

Construire des Hooks Personnalisés Réels

Construisons trois hooks personnalisés pratiques que tu utiliseras régulièrement dans de vrais projets.

useLocalStorage

Persiste le state dans localStorage pour qu'il survive aux rafraîchissements de page :

jsx
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.

useToggle

Un hook simple mais extrêmement courant pour un state booléen :

jsx
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>}
    </>
  );
}

useDebounce

Retarde la mise à jour d'une valeur jusqu'à ce que l'utilisateur arrête de la changer. Essentiel pour les champs de recherche :

jsx
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)} />;
}

Que Retourner d'un Hook Personnalisé

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.

Retourner un tableau — quand il y a 1-2 valeurs

En suivant le pattern de useState, retourne un tableau quand les appelants voudront probablement renommer les valeurs :

jsx
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);

Retourner un objet — quand il y a 3+ valeurs

Quand tu retournes beaucoup de valeurs liées, utilise un objet pour que les appelants puissent déstructurer seulement ce dont ils ont besoin :

jsx
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);

Retourner une valeur unique

Quand le hook calcule une seule valeur dérivée :

jsx
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.

Hooks Personnalisés en Action

html
<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é ?

Prêt à pratiquer ?

Crée ton compte gratuit pour accéder à l'éditeur de code interactif, lancer les défis et suivre ta progression.