Custom Hooks: Extracción y reutilización de lógica

Lectura
25 min~8 min lectura
CONCEPTO CLAVE: Los Custom Hooks son funciones de JavaScript que permiten extraer lógica reutilizable de componentes React. Su nombre debe comenzar obligatoriamente con "use" para que React pueda aplicar las reglas de los hooks automáticamente. Son la evolución natural de la composición cuando necesitas compartir estado y lógica entre componentes.

¿Qué son los Custom Hooks y por qué los necesitas?

Imagina que estás desarrollando una aplicación de comercio electrónico y tienes tres componentes diferentes: CarritoCompras, ListaDeseos y NotificacionCarrito. Cada uno necesita acceder a información del carrito de compras, calcular el total, y mostrar alertas cuando hay errores. Sin Custom Hooks, tendrías que duplicar esta lógica en cada componente o crear componentes wrappers innecesarios.

Los Custom Hooks resuelven este problema permitiéndote extraer lógica con estado en funciones reutilizables. Un hook personalizado es simplemente una función que puede usar otros hooks (como useState, useEffect, etc.) y devuelve datos o funciones que los componentes pueden consumir.

📌 Los Custom Hooks no son una característica mágica de React, son convenciones de nomenclatura. Cuando una función empieza con "use" y llama a otros hooks internamente, React sabe que debe aplicar las reglas de los hooks a esa función.

Reglas fundamentales de los Hooks

Antes de crear tu primer Custom Hook, recuerda las reglas esenciales que React aplica automáticamente a cualquier función cuyo nombre comience con "use":

ReglaDescripción¿Por qué importa?
Solo en nivel superiorNo llames hooks dentro de condicionales, bucles o funciones anidadasReact depende del orden de llamadas para mantener el estado
Solo en componentes ReactLos hooks solo deben llamarse desde componentes o Custom HooksGarantiza que el estado sea predecible
Orden consistenteEl mismo número de llamadas en cada renderizadoReact requiere orden estable para el estado interno
⚠️ Error común: No intentes usar un Custom Hook dentro de un condicional. Si necesitas lógica condicional, pon el condicional dentro del Custom Hook, no alrededor de su llamada.

Creando tu primer Custom Hook: useCarrito

Vamos a construir un hook completo paso a paso. Este hook gestionará todas las operaciones del carrito de compras con persistencia en localStorage.

  1. Define la interfaz del hook: Decide qué datos y funciones quieres exponer. Nuestro useCarrito necesitará el estado de productos, funciones para agregar/eliminar, y el total calculado.
  2. Implementa la lógica de estado: Usa useState para el carrito y useEffect para sincronizar con localStorage.
  3. Agrega la lógica de negocio: Valida productos, calcula totales, maneja errores.
  4. Expón lo necesario: Retorna un objeto con las funcionalidades que los componentes consumidores necesitan.
// hooks/useCarrito.js
import { useState, useEffect, useCallback } from 'react';

export function useCarrito() {
  const [carrito, setCarrito] = useState(() => {
    // Inicialización lazy: solo lee localStorage si existe
    const guardando = localStorage.getItem('carrito');
    return guardando ? JSON.parse(guardando) : [];
  });
  
  const [error, setError] = useState(null);

  // Sincronizar con localStorage cada vez que cambie
  useEffect(() => {
    localStorage.setItem('carrito', JSON.stringify(carrito));
  }, [carrito]);

  const agregarProducto = useCallback((producto, cantidad = 1) => {
    try {
      setError(null);
      setCarrito(prev => {
        const existente = prev.find(item => item.id === producto.id);
        
        if (existente) {
          // Incrementar cantidad si ya existe
          return prev.map(item =>
            item.id === producto.id
              ? { ...item, cantidad: item.cantidad + cantidad }
              : item
          );
        }
        
        // Agregar nuevo producto
        return [...prev, { ...producto, cantidad }];
      });
    } catch (err) {
      setError('No se pudo agregar el producto');
    }
  }, []);

  const eliminarProducto = useCallback((productoId) => {
    setError(null);
    setCarrito(prev => prev.filter(item => item.id !== productoId));
  }, []);

  const limpiarCarrito = useCallback(() => {
    setCarrito([]);
    setError(null);
  }, []);

  const total = carrito.reduce((sum, item) => {
    return sum + (item.precio * item.cantidad);
  }, 0);

  const cantidadTotal = carrito.reduce((sum, item) => {
    return sum + item.cantidad;
  }, 0);

  return {
    carrito,
    agregarProducto,
    eliminarProducto,
    limpiarCarrito,
    total,
    cantidadTotal,
    error,
    estaVacio: carrito.length === 0
  };
}
💡 Optimización: Usamos useCallback para las funciones que pasan como callbacks a componentes hijos. Esto evita re-renderizados innecesarios cuando el componente padre no ha cambiado.

Usando el Custom Hook en componentes

Una vez creado el hook, su uso es sorprendentemente simple. Los componentes que lo consuman solo necesitan llamarlo y usar los valores retornados:

// componentes/BotonAgregarCarrito.jsx
import { useCarrito } from '../hooks/useCarrito';

export function BotonAgregarCarrito({ producto }) {
  const { agregarProducto, cantidadTotal } = useCarrito();

  const handleClick = () => {
    agregarProducto(producto, 1);
  };

  return (
    <button onClick={handleClick}>
      Agregar al carrito ({cantidadTotal} items)
    </button>
  );
}

// componentes/PanelCarrito.jsx
export function PanelCarrito() {
  const { carrito, total, estaVacio, eliminarProducto, limpiarCarrito } = useCarrito();

  if (estaVacio) {
    return <div>Tu carrito está vacío</div>;
  }

  return (
    <div className="carrito-panel">
      <h3>Tu Carrito</h3>
      <ul>
        {carrito.map(item => (
          <li key={item.id}>
            {item.nombre} - ${item.precio} x {item.cantidad}
            <button onClick={() => eliminarProducto(item.id)}>×</button>
          </li>
        ))}
      </ul>
      <div className="total">Total: ${total.toFixed(2)}</div>
      <button onClick={limpiarCarrito}>Vaciar carrito</button>
    </div>
  );
}
📌 Resultado: Ahora tienes la lógica del carrito en un solo lugar. Si necesitas añadir una nueva funcionalidad (como cupones de descuento), solo modificas useCarrito y todos los componentes se benefician automáticamente.

Patrones avanzados de Custom Hooks

useToggle: Control de estado booleano

Uno de los hooks más simples pero útiles. Gestiona estados de apertura/cerrado como modales, menús desplegables, o elementos colapsables:

export function useToggle(valorInicial = false) {
  const [estado, setEstado] = useState(valorInicial);

  const toggle = useCallback(() => {
    setEstado(prev => !prev);
  }, []);

  const activar = useCallback(() => setEstado(true), []);
  const desactivar = useCallback(() => setEstado(false), []);

  return [estado, { toggle, activar, desactivar }];
}

// Uso en un componente:
function MenuDesplegable() {
  const [abierto, { toggle, activar, desactivar }] = useToggle(false);

  return (
    <div>
      <button onClick={toggle}>Menú</button>
      {abierto && (
        <ul onMouseLeave={desactivar}>
          <li>Opción 1</li>
          <li>Opción 2</li>
        </ul>
      )}
    </div>
  );
}

useFetch: Peticiones HTTP reutilizables

Gestionar estados de carga, errores y datos de peticiones API es tedioso. Un buen useFetch simplifica enormemente este proceso:

export function useFetch(url, opciones = {}) {
  const [estado, setEstado] = useState({
    datos: null,
    carga: true,
    error: null
  });

  useEffect(() => {
    let cancelada = false;

    async function fetchDatos() {
      try {
        setEstado(prev => ({ ...prev, carga: true, error: null }));
        
        const respuesta = await fetch(url, opciones);
        
        if (!respuesta.ok) {
          throw new Error(`Error ${respuesta.status}: ${respuesta.statusText}`);
        }
        
        const datos = await respuesta.json();
        
        if (!cancelada) {
          setEstado({ datos, carga: false, error: null });
        }
      } catch (err) {
        if (!cancelada) {
          setEstado({ datos: null, carga: false, error: err.message });
        }
      }
    }

    fetchDatos();

    return () => {
      cancelada = true;
    };
  }, [url, opciones.method, opciones.headers]);

  const reintentar = useCallback(() => {
    setEstado({ datos: null, carga: true, error: null });
  }, []);

  return { ...estado, reintentar };
}
💡 Cancelación de peticiones: El flag cancelada previene actualizar el estado después de que el componente se desmonte. Esto evita el temido error "Can't perform a React state update on an unmounted component".

Composición de Custom Hooks

Los hooks personalizados pueden usar otros hooks personalizados, creando abstracciones poderosas y reutilizables:

// Hook compuesto que usa useFetch y useToggle
export function useModalProducto(productoId) {
  const [modalAbierto, { activar, desactivar }] = useToggle(false);
  
  const { datos: producto, carga, error } = useFetch(
    productoId ? `/api/productos/${productoId}` : null
  );

  const abrirModal = useCallback(() => {
    if (productoId) activar();
  }, [productoId, activar]);

  return {
    producto,
    carga,
    error,
    modalAbierto,
    abrirModal,
    cerrarModal: desactivar
  };
}
Ver más

Ejemplo completo: useDebounce

Un hook esencial para optimizar búsquedas y evitar llamadas excesivas a APIs:

export function useDebounce(valor, delay = 300) {
  const [valorDebounced, setValorDebounced] = useState(valor);

  useEffect(() => {
    const timer = setTimeout(() => {
      setValorDebounced(valor);
    }, delay);

    return () => clearTimeout(timer);
  }, [valor, delay]);

  return valorDebounced;
}

// Uso para búsqueda con debounce:
function BarraBusqueda() {
  const [termino, setTermino] = useState('');
  const terminoDebounced = useDebounce(termino, 500);

  // Solo se ejecuta cuando el usuario deja de escribir por 500ms
  const { datos: resultados, carga } = useFetch(
    `/api/buscar?q=${terminoDebounced}`
  );

  return (
    <div>
      <input 
        value={termino}
        onChange={e => setTermino(e.target.value)}
        placeholder="Buscar..."
      />
      {carga && <Spinner />}
      {resultados && <ListaResultados resultados={resultados} />}
    </div>
  );
}

Buenas prácticas para Custom Hooks

PrácticaRecomendaciónEjemplo
NomenclaturaSiempre "use" al iniciouseFormulario, useAuth
Responsabilidad únicaUn hook, un propósitoSeparar validación de persistencia
Parámetros clarosTypedefs o JSDoc para propsDocumentar opciones configurables
Inicialización lazyFunciones para estados iniciales costososuseState(() => localStorage.get())
Retornar objetosMás flexible que arrays{ valor, setValor } vs [valor, setValor]
⚠️ Evita esto: No crees hooks que solo wrapeen una función sin estado. Si no usas hooks internos, no necesitas un Custom Hook. Una función normal es más apropiada.
La magia de los Custom Hooks no está en su sintaxis, sino en cómo te permiten compartir estado e inteligencia entre componentes sin necesidad de patrones complejos de herencia o context providers excesivos.

Resumen y próximo paso

Los Custom Hooks son la herramienta definitiva para compartir lógica con estado en React. We've aprendido a:

  • Crear hooks que encapsulan lógica compleja y estado
  • Reutilizar lógica entre múltiples componentes sin duplicación
  • Componer hooks para crear abstracciones más sofisticadas
  • Aplicar patrones avanzados como debounce y cancelamiento de peticiones
🧠 Quiz de comprensión

¿Cuál es la principal ventaja de usar Custom Hooks sobre simplemente copiar y pegar código de lógica entre componentes?

  • A) Los Custom Hooks hacen que el código sea más rápido
  • B) Los Custom Hooks permiten extraer y compartir lógica con estado de forma centralizada, facilitando el mantenimiento y las actualizaciones
  • C) Los Custom Hooks son obligatorios en React moderno
  • D) Los Custom Hooks eliminan la necesidad de usar useState
✅ Respuesta correcta: B) La principal ventaja es la reutilización de lógica con estado. Cuando la lógica cambia (por ejemplo, añadir validación), solo necesitas actualizarla en el Custom Hook y todos los componentes se benefician automáticamente.

En la próxima lección exploraremos Context API y composición, donde aprenderás a compartir datos globales eficientemente y combinarlo con los Custom Hooks que dominas ahora.