En esta página

Hooks Personalizados

14 min lectura TextoCap. 3 — Patrones y navegación

¿Qué es un hook personalizado?

Un hook personalizado es una función JavaScript que:

  1. Su nombre comienza con use
  2. Puede llamar a otros hooks (useState, useEffect, useRef, etc.)
  3. Extrae y encapsula lógica de estado reutilizable

Los hooks personalizados no son una API especial de React: son simplemente el patrón de extraer lógica de estado a funciones reutilizables.

// Sin hook personalizado: lógica mezclada en el componente
function Perfil() {
  const [online, setOnline] = useState(navigator.onLine);

  useEffect(() => {
    const manejar = () => setOnline(navigator.onLine);
    window.addEventListener('online', manejar);
    window.addEventListener('offline', manejar);
    return () => {
      window.removeEventListener('online', manejar);
      window.removeEventListener('offline', manejar);
    };
  }, []);

  return <p>{online ? '🟢 En línea' : '🔴 Sin conexión'}</p>;
}

// Con hook personalizado: lógica extraída y reutilizable
function useEstadoRed(): boolean {
  const [online, setOnline] = useState(navigator.onLine);

  useEffect(() => {
    const manejar = () => setOnline(navigator.onLine);
    window.addEventListener('online', manejar);
    window.addEventListener('offline', manejar);
    return () => {
      window.removeEventListener('online', manejar);
      window.removeEventListener('offline', manejar);
    };
  }, []);

  return online;
}

// Ahora cualquier componente puede usar la lógica
function Perfil() {
  const online = useEstadoRed();
  return <p>{online ? '🟢 En línea' : '🔴 Sin conexión'}</p>;
}

Las reglas de los hooks

React impone dos reglas fundamentales que deben respetarse siempre:

Regla 1: Solo llama hooks en el nivel superior

// ❌ INCORRECTO: hook condicional
function Componente({ activo }: { activo: boolean }) {
  if (activo) {
    const [valor, setValor] = useState(0); // Rompe la regla
  }
}

// ✅ CORRECTO: hook siempre en el nivel superior
function Componente({ activo }: { activo: boolean }) {
  const [valor, setValor] = useState(0);

  if (!activo) return null; // La condición va después del hook
}

Regla 2: Solo llama hooks desde componentes React o hooks personalizados

// ❌ INCORRECTO: hook en función regular
function calcularTotal(items: Item[]) {
  const [descuento] = useState(0); // Error
  return items.reduce((sum, i) => sum + i.precio, 0) * (1 - descuento);
}

// ✅ CORRECTO: hook en componente o hook personalizado
function useCalcularTotal(items: Item[]) {
  const [descuento] = useState(0);
  return items.reduce((sum, i) => sum + i.precio, 0) * (1 - descuento);
}

Hook useContadorCaracteres

interface ResultadoContador {
  valor: string;
  longitud: number;
  restantes: number;
  porcentaje: number;
  onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
}

function useContadorCaracteres(limite: number, inicial = ''): ResultadoContador {
  const [valor, setValor] = useState(inicial);

  const onChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
    const nuevoValor = e.target.value.slice(0, limite);
    setValor(nuevoValor);
  };

  return {
    valor,
    longitud: valor.length,
    restantes: limite - valor.length,
    porcentaje: Math.round((valor.length / limite) * 100),
    onChange,
  };
}

// Uso
function FormularioBio(): React.JSX.Element {
  const bio = useContadorCaracteres(160);

  return (
    <div>
      <textarea value={bio.valor} onChange={bio.onChange} placeholder="Tu biografía…" />
      <p style={{ color: bio.restantes < 20 ? 'red' : 'inherit' }}>
        {bio.restantes} caracteres restantes ({bio.porcentaje}%)
      </p>
    </div>
  );
}

Hook useToggle

function useToggle(inicial = false): [boolean, () => void, (v: boolean) => void] {
  const [valor, setValor] = useState(inicial);
  const toggle = () => setValor((v) => !v);
  return [valor, toggle, setValor];
}

function ModalEjemplo(): React.JSX.Element {
  const [abierto, toggleModal, setModal] = useToggle(false);

  return (
    <>
      <button type="button" onClick={toggleModal}>Abrir modal</button>
      {abierto && (
        <div role="dialog" aria-modal="true">
          <h2>Modal</h2>
          <button type="button" onClick={() => setModal(false)}>Cerrar</button>
        </div>
      )}
    </>
  );
}

Hook usePaginacion

interface ResultadoPaginacion {
  pagina: number;
  totalPaginas: number;
  irA: (pagina: number) => void;
  siguiente: () => void;
  anterior: () => void;
  puedeIrAnterior: boolean;
  puedeIrSiguiente: boolean;
}

function usePaginacion(totalItems: number, itemsPorPagina: number): ResultadoPaginacion {
  const [pagina, setPagina] = useState(1);
  const totalPaginas = Math.ceil(totalItems / itemsPorPagina);

  const irA = (nuevaPagina: number) => {
    setPagina(Math.min(Math.max(1, nuevaPagina), totalPaginas));
  };

  return {
    pagina,
    totalPaginas,
    irA,
    siguiente: () => irA(pagina + 1),
    anterior: () => irA(pagina - 1),
    puedeIrAnterior: pagina > 1,
    puedeIrSiguiente: pagina < totalPaginas,
  };
}

Composición de hooks — el patrón más poderoso

La verdadera potencia emerge cuando compones hooks entre sí:

// useBusquedaConHistorial = useDebounce + useFetch + useLocalStorage
function useBusquedaConHistorial(endpoint: string) {
  const [consulta, setConsulta] = useState('');
  const [historial, setHistorial] = useLocalStorage<string[]>('historial-busqueda', []);
  const consultaDebounced = useDebounce(consulta, 400);
  const url = consultaDebounced ? `${endpoint}?q=${consultaDebounced}` : null;
  const { datos, cargando, error } = useFetch<unknown[]>(url);

  const buscar = (termino: string) => {
    setConsulta(termino);
    setHistorial((prev) =>
      [termino, ...prev.filter((h) => h !== termino)].slice(0, 10)
    );
  };

  return { consulta, buscar, historial, datos, cargando, error };
}

Los hooks personalizados son la forma idiomática de React para compartir lógica compleja sin componentes HOC o render props. Úsalos siempre que encuentres lógica de estado que se repite en varios componentes.

Los hooks deben empezar con use
La convención use como prefijo no es solo de estilo: React (y el linter) la usa para identificar hooks y aplicar las reglas de hooks. Un hook sin el prefijo use no será detectado por el linter y podrías usarlo incorrectamente (condicionalmente o en bucles).
Reglas de hooks — nunca las rompas
Los hooks solo pueden llamarse en el nivel superior de un componente funcional o de otro hook personalizado. Nunca dentro de condicionales (if), bucles (for/while) o funciones anidadas. Esta restricción garantiza que el orden de hooks sea siempre el mismo entre renders.
Composición de hooks — la clave de la reutilización
Los hooks personalizados pueden llamar a otros hooks personalizados. Esta composición es la forma más poderosa de reutilizar lógica compleja. useDebounce + useFetch + useLocalStorage pueden componerse en un hook useBusquedaPersistente sin que ningún componente sepa de los detalles.