← React & TypeScript

Hooks

¿Qué es un custom hook y cuándo crearías uno? ¿Dos componentes que usan el mismo custom hook comparten estado?

Ver respuesta — intenta responderla en voz alta primero

Un custom hook es una función que empieza con use y que llama a otros hooks por dentro, para extraer lógica con estado y reutilizarla entre componentes. Lo que se comparte es la LÓGICA, no el estado: cada componente que lo llama recibe su propia copia independiente del estado.

Cuando dos o tres componentes repiten el mismo patrón de useState + useEffect (escuchar el tamaño de la ventana, leer localStorage, hacer debounce), esa lógica se puede extraer a una función. Como esa función llama hooks, debe cumplir las reglas de los hooks, y por convención (que el linter usa para verificarte) su nombre empieza con use.

Los puntos clave:

  • Es solo una función de JavaScript que usa hooks adentro. No hay magia: React la trata como parte del componente que la llama.
  • Convención use*: obligatoria en la práctica; le dice al linter (y a otros devs) que aplican las reglas de los hooks.
  • Comparte lógica, NO estado. Cada llamada al hook crea instancias nuevas de sus useState/useEffect. Si ComponenteA y ComponenteB usan useContador(), cada uno tiene su propio contador. Para compartir estado real necesitas Context o levantar el estado.
  • Cuándo crear uno: cuando repites lógica con estado en 2+ componentes, o cuando un componente crece tanto que separar la lógica de la vista lo hace legible y testeable.
  • Cuándo NO: si la función no usa hooks (es una utilidad pura como formatearFecha), no la llames useAlgo; es solo una función normal.
import { useEffect, useState } from "react";

// Custom hook: encapsula la suscripción al resize de la ventana
function useVentanaAncho(): number {
  const [ancho, setAncho] = useState<number>(() => window.innerWidth);

  useEffect(() => {
    const alRedimensionar = () => setAncho(window.innerWidth);
    window.addEventListener("resize", alRedimensionar);
    // Cleanup: el hook limpia lo que creó
    return () => window.removeEventListener("resize", alRedimensionar);
  }, []);

  return ancho;
}

function Encabezado() {
  const ancho = useVentanaAncho(); // instancia propia
  return <h1>{ancho < 768 ? "Menú compacto" : "Menú completo"}</h1>;
}
function PieDePagina() {
  const ancho = useVentanaAncho(); // OTRA instancia, estado independiente
  return <footer>Ancho actual: {ancho}px</footer>;
}
// Comportamiento esperado: ambos reaccionan al resize, pero cada uno
// mantiene SU propio estado: comparten la lógica, no la variable "ancho".

Creer que el custom hook es un almacén global y que todos los componentes ven el mismo estado:

function useContador() {
  const [valor, setValor] = useState(0);
  return { valor, incrementar: () => setValor(v => v + 1) };
}
// MAL (como expectativa): "si incremento en A, B también cambia"
function A() {
  const { valor, incrementar } = useContador();
  return <button onClick={incrementar}>A: {valor}</button>;
}
function B() {
  const { valor } = useContador();
  return <p>B: {valor}</p>; // B SIEMPRE muestra 0: es otra instancia
}

// BIEN: para compartir el MISMO estado, se levanta al padre (o Context)
function Padre() {
  const { valor, incrementar } = useContador(); // una sola instancia
  return <><button onClick={incrementar}>A: {valor}</button><p>B: {valor}</p></>;
}

Otro tropiezo: llamar useAlgo a una función que no usa hooks, o llamar hooks dentro de una función sin el prefijo use; el linter necesita la convención para protegerte.

Un custom hook es una función que empieza con use y llama hooks por dentro, y sirve para extraer lógica con estado y reutilizarla, por ejemplo un useVentanaAncho o un useLocalStorage. Crearía uno cuando veo el mismo patrón de useState y useEffect repetido en varios componentes, o para separar la lógica de la vista y poder testearla. El matiz importante es que comparte lógica, no estado: cada componente que llama el hook obtiene su propia instancia de estado independiente. Si necesito que varios componentes vean el mismo dato, levanto el estado a un padre o uso Context.

Reto rápido

Implementa useLocalStorage<T>(clave: string, valorInicial: T) que devuelva [valor, setValor] como useState, pero persistiendo el valor en localStorage.

Ver solución
import { useState } from "react";

function useLocalStorage<T>(clave: string, valorInicial: T): [T, (nuevo: T) => void] {
  // Inicialización perezosa: leemos localStorage solo una vez
  const [valor, setValorInterno] = useState<T>(() => {
    const guardado = localStorage.getItem(clave);
    return guardado !== null ? (JSON.parse(guardado) as T) : valorInicial;
  });

  function setValor(nuevo: T) {
    setValorInterno(nuevo);
    localStorage.setItem(clave, JSON.stringify(nuevo));
  }

  return [valor, setValor];
}

// Uso: misma forma que useState, pero sobrevive recargas de página
function Preferencias() {
  const [tema, setTema] = useLocalStorage<"claro" | "oscuro">("tema", "claro");
  const alternar = () => setTema(tema === "claro" ? "oscuro" : "claro");
  return <button onClick={alternar}>Tema actual: {tema}</button>;
}