Kodokon kodokon.com

Hooks personalizados: extraer y reutilizar lógica

Extrae la lógica con estado en hooks reutilizables, componibles y testeables.

8 min · 3 preguntas

Abrir esta lección en Kodokon

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

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;
}
La limpieza cancela el temporizador anterior en cada cambio.

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.

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];
}
Inicialización perezosa: localStorage solo se lee al montar.

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.

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;
}
El hook de negocio se apoya en el hook genérico.

Prueba de conocimientos

Comprueba que has retenido los puntos clave de esta lección.

  1. ¿Para qué sirve realmente el prefijo use de un hook personalizado?
    • Es una convención puramente estética heredada de las clases
    • Permite al linter identificar la función como un hook y comprobar en ella las reglas de los hooks
    • React se niega a ejecutar una función con estado sin este prefijo
    • Dispara la importación automática desde react
  2. Dos componentes llaman a useLocalStorage con la misma clave. ¿Comparten su estado?
    • Sí: el hook sincroniza automáticamente a todos quienes lo llaman
    • No, y los datos se persisten bajo dos claves de almacenamiento distintas
    • No: cada llamada posee su propio estado de React; solo se comparten los datos en localStorage
  3. En useDebouncedValue, ¿cuál es el papel de la función de limpieza devuelta por useEffect?
    • Cancelar el temporizador pendiente para que solo el último valor tecleado se publique tras el retardo
    • Restablecer el valor debounced cada vez que el componente se desmonta
    • Bloquear los re-renders del componente durante toda la duración del retardo