Skip to content
This page has been auto-translated and may contain errors.View in English

Custom hooks en la práctica

El capítulo de Hooks introdujo la idea: un custom hook es una función que empieza con use y compone hooks en lógica reutilizable. Este capítulo es sobre cómo se ve eso en la práctica, y el curso lo demuestra de manera contundente: después de toda una sección construyendo un componente Toggle sin render, un custom hook lo reemplaza en una fracción del código, y el componente se elimina.

La línea que vale la pena trazar con precisión es contra una función utilitaria ordinaria. Una función que trae datos o formatea una fecha es una utilidad. Un custom hook llama otros hooks, por eso las reglas de hooks aplican dentro de él.

Extraer un efecto: useEffectOnUpdate

El componente Toggle terminó con un chunk de lógica cuyo trabajo era "ejecutar este callback cuando el estado cambia, pero nunca en el primer render": un useEffect protegido por una ref firstRender. Eso no es lógica de toggle; es lógica de efecto, útil en cualquier lado. Extraerla produce un hook con la misma firma que useEffect:

jsx
export default function useEffectOnUpdate(effectFunction, deps) {
  const firstRender = useRef(true)

  useEffect(() => {
    if (firstRender.current) {
      firstRender.current = false
    } else {
      effectFunction()
    }
  }, deps)
}

Quienes lo usan escriben useEffectOnUpdate(onToggle, [on]) y la gestión del primer render desaparece de su código. Los hooks que envuelven otros hooks de esta manera vale la pena construirlos incluso cuando la lógica envuelta es pocas líneas, porque el nombre comunica la intención.

La protección tiene un detalle solo en desarrollo

El mismo que flagueó render props: StrictMode gasta la ref en su primera ejecución de efecto, así que el callback se dispara en la segunda. Los builds de producción no hacen esto, y la solución duradera es comparar el valor contra el anterior en lugar de rastrear si este es el primer render.

Extraer estado: useToggle

useEffectOnUpdate no retorna nada, como useEffect. La mayoría de los custom hooks devuelven algo, como useState lo hace, y la forma de lo que retornan es una decisión de diseño. useToggle reúne el booleano, la función flip y el callback de cambio en un solo lugar:

jsx
export default function useToggle({ initialValue = false, onToggle = () => {} } = {}) {
  const [on, setOn] = useState(initialValue)
  const toggle = () => setOn(prevOn => !prevOn)

  useEffectOnUpdate(onToggle, [on])

  return [on, toggle]
}
jsx
const [on, toggle] = useToggle({ initialValue: true })

Cada llamada a useToggle obtiene su propio useState, así que dos usuarios tienen dos booleanos independientes, ya sea que sean dos componentes o un componente llamando al hook dos veces.

Tres decisiones ahí, cada una un patrón que reutilizarás:

  • Retorna un array cuando los valores son un par natural que el usuario renombrará, exactamente como useState: un usuario destructura [on, toggle], otro [open, toggleOpen]. Retorna un objeto cuando hay varios valores y los nombres importan más que el orden.
  • Toma un objeto de configuración en lugar de parámetros posicionales. Con useToggle(false, callback) un usuario que solo quiere el callback está forzado a suministrar el booleano primero; con { initialValue, onToggle } pasan lo que significan y saltan el resto, y cada opción puede llevar un default.
  • Defaultea el callback a una noop (() => {}) así el hook puede llamar onToggle() sin condiciones sin crashes en usuarios que nunca pasaron uno.

La recompensa: eliminar el componente

Con useToggle a la mano, el widget de estrella deja de necesitar completamente la maquinaria Toggle:

jsx
export default function Star({ onChange }) {
  const [on, toggle] = useToggle({ onToggle: onChange })

  return on
    ? <BsStarFill className="star filled" onClick={toggle} />
    : <BsStar className="star" onClick={toggle} />
}

El menú mantiene su forma de componente compuesto, porque sus piezas aún necesitan estado implícito compartido, pero su proveedor ahora obtiene sus valores del hook: Menu llama useToggle, pasa { open, toggleOpen } a través de su propio context, y los archivos del componente Toggle sin render se eliminan.

Esa eliminación es la lección real de la sección. El componente sin render se ganó su lugar mientras muchos componentes compuestos podrían haberlo compartido; una vez que la lógica cabía en un hook, el hook era la herramienta más simple, y sacar código funcionando del que estabas orgulloso es una parte normal del trabajo.

Los dos patrones se complementan: un hook comparte lógica, un componente sin render comparte lógica a través del árbol vía context y composición. Llega al hook primero, y deja que un componente se gane la ceremonia extra. El curso cierra esta sección con un proyecto en solitario, Component Library++, que pone cada patrón de estos cinco capítulos en una librería.

Un hábito que vale la pena formar temprano: antes de escribir un custom hook, verifica si la comunidad ya tiene uno. Valores debounced, media queries, estado de local storage, event listeners, rastreo de valor anterior, todos estos existen como hooks probados en librerías pequeñas, y leer su código es una forma rápida de absorber los idiomas. El tuyo propio sigue ganando cuando la lógica es específica de tu app, y hooks específicos de la app como useCurrentUser o useCartTotal son a menudo la capa más limpia donde la lógica de datos de una app puede vivir: los componentes se mantienen declarativos, y las partes desordenadas se quedan en una función nombrada, testeable.

Los custom hooks comparten lógica; no comparten estado. Cuando dos usuarios deben ver el mismo valor, el hook solo no puede hacerlo: el estado tiene que vivir una sola vez, en un proveedor, y el hook se convierte en el lector, el patrón useTheme del capítulo de context. Mantener "lógica reutilizable" y "estado compartido" distintos en tu cabeza previene el error clásico de esperar que un hook actúe como un store.

Dos detalles deciden si hooks como estos son agradables de entregar. El primero es identidad: useToggle construye una función toggle nueva en cada render, así que un usuario que la pasa a un memoized hijo o a un dependency array de otro hook entrega un valor nuevo cada vez. Envuélvela en useCallback dentro del hook cuando los usuarios dependen de una referencia estable.

El segundo es tooling. Reenviar un array deps a través de un hook envoltorio funciona en runtime mientras su longitud sea constante, pero exhaustive-deps no puede verificar un array que no vio escrito, e ignora useEffectOnUpdate(onToggle, [on]) completamente en el sitio de la llamada a menos que el proyecto liste el hook en la opción additionalHooks de la regla. Una codebase que se inclina en hooks envolventes debe añadir esa configuración, así los warnings que el capítulo de effects te dijo que trates como bugs reales te siguen alcanzando.

JunoEmbotella la lógica, reutilízala en cualquier lado Un custom hook es una función cuyo nombre empieza con use y que se construye sobre hooks como useState para empaquetar un pedazo de lógica. Una vez que useToggle existe, cualquier componente puede obtener un valor encendido-apagado y una función flip en una línea, de la misma forma que useState te entrega un valor y un setter.

Cada componente que lo llama obtiene su propia copia del estado, así que un hook es una receta que reutilizas en lugar de un valor que todos comparten.

JunoEmbotella la lógica, reutilízala en cualquier lado Extrae lógica con estado repetida en hooks: useEffectOnUpdate para efectos que saltan el primer render, useToggle para estado booleano con un callback onToggle.

Retorna un array cuando los usuarios renombrarán un par natural, toma un objeto de configuración con defaults en lugar de parámetros posicionales, y defaultea los callbacks a una noop.

Prefiere un hook sobre un componente sin render hasta que la lógica tenga que viajar a través del árbol.

JunoEmbotella la lógica, reutilízala en cualquier lado Un custom hook compone hooks, hereda sus reglas transitivamente, y comparte lógica mientras cada usuario mantiene estado independiente; empareja lo con un proveedor cuando el estado debe ser compartido en lugar de duplicado.

Dos detalles deciden si uno es agradable de entregar: las funciones que retorna se reconstruyen en cada render, así que envuélvelas en useCallback cuando los usuarios las pasan a hijos memoized o a dependency arrays, y un hook envoltorio que reenvía un array deps se mantiene invisible a exhaustive-deps hasta que el proyecto lo nombra en la opción additionalHooks de la regla.

Siguiente: Routing, donde la URL empieza a decidir qué componentes se renderizan.