Kodokon kodokon.com

Hooks personnalisés : extraire et réutiliser la logique

Extrayez la logique à état dans des hooks réutilisables, composables et testables.

8 min · 3 questions

Ouvrir cette leçon dans Kodokon

Un hook personnalisé est une fonction dont le nom commence par use et qui appelle d'autres hooks. Il extrait de la logique à état, pas de l'état partagé : chaque composant qui l'appelle obtient ses propres useState et useEffect, totalement isolés. C'est l'outil de factorisation le plus direct de React - là où les HOC et les render props ajoutaient des couches dans l'arbre, un hook s'insère dans le corps du composant, se compose et se teste facilement.

JAVASCRIPT
import { useEffect, useState } from "react";

function useDebouncedValue(value, delay = 300) {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const id = setTimeout(() => {
      setDebounced(value);
    }, delay);
    return () => clearTimeout(id);
  }, [value, delay]);

  return debounced;
}
Le nettoyage annule le timer précédent à chaque changement.

Le préfixe use n'est pas décoratif : c'est lui qui permet au plugin ESLint react-hooks de vérifier, dans votre fonction, les règles des hooks - appels inconditionnels, au niveau racine. Soignez aussi la forme de l'API retournée : un tuple [value, setValue] quand l'usage calque useState, un objet à clés nommées dès qu'il y a plus de deux éléments à exposer.

JAVASCRIPT
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    const stored = localStorage.getItem(key);
    return stored !== null
      ? JSON.parse(stored)
      : initialValue;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}
Initialisation paresseuse : localStorage n'est lu qu'au montage.

Les hooks se composent : un hook peut en appeler d'autres, y compris les vôtres. C'est ainsi que naissent des abstractions métier lisibles, construites sur des briques génériques. Notez le couple AbortController et fonction de nettoyage : sans lui, une requête lancée pour une ancienne saisie peut répondre après la plus récente et écraser son résultat - la race condition classique des champs de recherche.

JAVASCRIPT
function useUserSearch(query) {
  const debounced = useDebouncedValue(query, 400);
  const [results, setResults] = useState([]);

  useEffect(() => {
    if (debounced === "") {
      setResults([]);
      return;
    }
    const controller = new AbortController();
    fetch("/api/search?q=" + debounced, {
      signal: controller.signal,
    })
      .then((res) => res.json())
      .then(setResults)
      .catch(() => {});
    return () => controller.abort();
  }, [debounced]);
  return results;
}
Le hook métier s'appuie sur le hook générique.

Quiz de validation

Vérifiez que vous avez bien retenu les points clés de cette leçon.

  1. À quoi sert réellement le préfixe use d'un hook personnalisé ?
    • C'est une convention purement esthétique héritée des classes
    • Il permet au linter d'identifier la fonction comme un hook et d'y vérifier les règles des hooks
    • React refuse d'exécuter une fonction à état sans ce préfixe
    • Il déclenche l'import automatique depuis react
  2. Deux composants appellent useLocalStorage avec la même clé. Partagent-ils leur état ?
    • Oui : le hook synchronise automatiquement tous ses appelants
    • Non, et la donnée est persistée dans deux clés de stockage différentes
    • Non : chaque appel possède son propre état React ; seule la donnée dans localStorage est commune
  3. Dans useDebouncedValue, quel est le rôle de la fonction de nettoyage retournée par useEffect ?
    • Annuler le timer en attente pour que seule la dernière valeur saisie soit publiée après le délai
    • Réinitialiser la valeur débouncée à chaque démontage du composant
    • Bloquer les re-rendus du composant pendant toute la durée du délai