Extrayez la logique à état dans des hooks réutilisables, composables et testables.
Ouvrir cette leçon dans KodokonUn 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.
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 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.
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];
}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.
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;
}