¿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.
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":
| Regla | Descripción | ¿Por qué importa? |
|---|---|---|
| Solo en nivel superior | No llames hooks dentro de condicionales, bucles o funciones anidadas | React depende del orden de llamadas para mantener el estado |
| Solo en componentes React | Los hooks solo deben llamarse desde componentes o Custom Hooks | Garantiza que el estado sea predecible |
| Orden consistente | El mismo número de llamadas en cada renderizado | React requiere orden estable para el estado interno |
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.
- Define la interfaz del hook: Decide qué datos y funciones quieres exponer. Nuestro
useCarritonecesitará el estado de productos, funciones para agregar/eliminar, y el total calculado. - Implementa la lógica de estado: Usa
useStatepara el carrito yuseEffectpara sincronizar con localStorage. - Agrega la lógica de negocio: Valida productos, calcula totales, maneja errores.
- 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
};
}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>
);
}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 };
}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ásEjemplo 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áctica | Recomendación | Ejemplo |
|---|---|---|
| Nomenclatura | Siempre "use" al inicio | useFormulario, useAuth |
| Responsabilidad única | Un hook, un propósito | Separar validación de persistencia |
| Parámetros claros | Typedefs o JSDoc para props | Documentar opciones configurables |
| Inicialización lazy | Funciones para estados iniciales costosos | useState(() => localStorage.get()) |
| Retornar objetos | Más flexible que arrays | { valor, setValor } vs [valor, setValor] |
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
¿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
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.