Extrae la lógica con estado en hooks reutilizables, componibles y testeables.
Abrir esta lección en KodokonUn hook personalizado es una función cuyo nombre empieza por use y que llama a otros hooks. Extrae lógica con estado, no estado compartido: cada componente que lo llama obtiene su propio useState y useEffect, totalmente aislados. Es la herramienta más directa de React para factorizar lógica - donde los HOC y las render props añadían capas al árbol, un hook se desliza en el cuerpo del componente, se compone con facilidad y es fácil de testear.
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;
}El prefijo use no es decorativo: es lo que permite al plugin de ESLint react-hooks comprobar las reglas de los hooks dentro de tu función - llamadas incondicionales, en el nivel superior. Cuida también la forma de la API que devuelves: una tupla [value, setValue] cuando el uso refleja useState, un objeto con claves con nombre en cuanto haya más de dos cosas que exponer.
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];
}Los hooks se componen: un hook puede llamar a otros hooks, incluidos los tuyos. Así es como emergen abstracciones de negocio legibles, construidas sobre bloques genéricos. Fíjate en el emparejamiento de AbortController con la función de limpieza: sin él, una petición lanzada para una entrada más antigua puede responder después de la más reciente y sobrescribir su resultado - la clásica race condition de los campos de búsqueda.
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;
}