Los hooks son funciones especiales de React que permiten añadir estado y otras capacidades a los componentes de función. Antes de que existieran los hooks (React 16.8, 2019), para tener estado o efectos secundarios necesitabas clases. Hoy, casi todo el código React moderno se escribe con hooks. Los dos más fundamentales son useState para gestionar estado y useEffect para sincronizar el componente con el mundo exterior.
En esta guía aprenderás cómo funcionan en profundidad, cuáles son los errores más habituales y cómo usarlos en situaciones reales.
useState en profundidad
useState añade una variable de estado a un componente. Cuando esa variable cambia, React re-renderiza el componente con el nuevo valor.
import { useState } from 'react';
// Sintaxis básica
const [valor, setValor] = useState(valorInicial);
// valor → el valor actual del estado
// setValor → función para actualizarlo (desencadena un re-render)
// valorInicial → el valor al montar el componente (solo se usa la primera vez)
Tipos de estado habituales
// Estado primitivo
const [contador, setContador] = useState(0);
const [nombre, setNombre] = useState('');
const [activo, setActivo] = useState(false);
const [precio, setPrecio] = useState(null);
// Estado de objeto
const [usuario, setUsuario] = useState({
nombre: '',
email: '',
rol: 'visitante'
});
// Estado de array
const [tareas, setTareas] = useState([]);
// Estado para la UI
const [cargando, setCargando] = useState(false);
const [error, setError] = useState(null);
const [modal, setModal] = useState(false);
Actualizar el estado correctamente
// ─── Primitivos: directo ──────────────────────────────────────────────────────
const [contador, setContador] = useState(0);
setContador(5); // asignar un valor nuevo
setContador(contador + 1); // basado en el valor actual
// Forma funcional: más segura cuando el nuevo valor depende del anterior
// React puede agrupar varios setStates y la forma funcional garantiza
// que siempre usas el valor más reciente
setContador(prev => prev + 1);
// ─── Objetos: nunca mutarlos, siempre crear uno nuevo ─────────────────────────
const [usuario, setUsuario] = useState({ nombre: 'Ana', email: 'ana@mail.com' });
// ❌ Mutación directa: React no detecta el cambio
usuario.nombre = 'Carlos';
setUsuario(usuario); // mismo objeto → React no re-renderiza
// ✅ Crear nuevo objeto con spread
setUsuario({ ...usuario, nombre: 'Carlos' });
// Actualizar campo dinámico (útil en formularios)
function actualizarCampo(campo, valor) {
setUsuario(prev => ({ ...prev, [campo]: valor }));
}
// ─── Arrays: siempre crear uno nuevo ──────────────────────────────────────────
const [items, setItems] = useState([1, 2, 3]);
// Añadir elemento
setItems([...items, 4]);
setItems(prev => [...prev, 4]);
// Eliminar elemento
setItems(items.filter(item => item !== 2));
// Actualizar un elemento
setItems(items.map(item => item === 2 ? 99 : item));
// Reordenar (ejemplo: mover el primer elemento al final)
const [primero, ...resto] = items;
setItems([...resto, primero]);
Estado inicial perezoso (lazy initialization)
// Si el valor inicial requiere un cálculo costoso, pasa una función
// React solo la ejecuta la primera vez, no en cada re-render
// ❌ Se ejecuta en cada render aunque solo se use la primera vez
const [datos, setDatos] = useState(calcularDatosComplejos());
// ✅ Se ejecuta solo una vez al montar el componente
const [datos, setDatos] = useState(() => calcularDatosComplejos());
// Ejemplo real: leer de localStorage
const [tema, setTema] = useState(() => {
return localStorage.getItem('tema') || 'claro';
});
Ejemplo real: formulario controlado
import { useState } from 'react';
function FormularioRegistro() {
const [campos, setCampos] = useState({
nombre: '',
email: '',
contraseña: '',
});
const [errores, setErrores] = useState({});
const [enviado, setEnviado] = useState(false);
function actualizarCampo(e) {
const { name, value } = e.target;
setCampos(prev => ({ ...prev, [name]: value }));
// Limpiar el error del campo al escribir
if (errores[name]) {
setErrores(prev => ({ ...prev, [name]: null }));
}
}
function validar() {
const nuevosErrores = {};
if (!campos.nombre.trim())
nuevosErrores.nombre = 'El nombre es obligatorio';
if (!campos.email.includes('@'))
nuevosErrores.email = 'El email no es válido';
if (campos.contraseña.length < 8)
nuevosErrores.contraseña = 'Mínimo 8 caracteres';
return nuevosErrores;
}
async function manejarEnvio(e) {
e.preventDefault();
const erroresEncontrados = validar();
if (Object.keys(erroresEncontrados).length > 0) {
setErrores(erroresEncontrados);
return;
}
// Aquí iría la llamada a la API
setEnviado(true);
}
if (enviado) {
return <p>¡Registro completado! Revisa tu email.</p>;
}
return (
<form onSubmit={manejarEnvio}>
<div>
<input
name="nombre"
value={campos.nombre}
onChange={actualizarCampo}
placeholder="Tu nombre"
/>
{errores.nombre && <span className="error">{errores.nombre}</span>}
</div>
<div>
<input
name="email"
type="email"
value={campos.email}
onChange={actualizarCampo}
placeholder="tu@email.com"
/>
{errores.email && <span className="error">{errores.email}</span>}
</div>
<div>
<input
name="contraseña"
type="password"
value={campos.contraseña}
onChange={actualizarCampo}
placeholder="Contraseña"
/>
{errores.contraseña && <span className="error">{errores.contraseña}</span>}
</div>
<button type="submit">Registrarse</button>
</form>
);
}
useEffect en profundidad
useEffect permite sincronizar tu componente con algo externo: una API, el DOM, un temporizador, una suscripción. Es el lugar correcto para hacer cosas que no son renderizado puro.
import { useEffect } from 'react';
useEffect(
() => {
// Código del efecto: se ejecuta después de renderizar
// Aquí puedes hacer peticiones, suscribirte a eventos, etc.
return () => {
// Función de limpieza (opcional):
// se ejecuta antes de que el efecto se vuelva a ejecutar
// y cuando el componente se desmonta
};
},
[dependencia1, dependencia2] // array de dependencias
);
El array de dependencias: la clave de useEffect
// 1. Sin array: se ejecuta después de CADA render
useEffect(() => {
console.log('Después de cada render');
});
// Rara vez lo quieres así: puede causar bucles infinitos fácilmente
// 2. Array vacío []: se ejecuta solo al montar el componente
useEffect(() => {
console.log('Solo al montar (equivale a componentDidMount)');
// Ideal para: cargar datos iniciales, suscripciones únicas
}, []);
// 3. Con dependencias: se ejecuta cuando alguna dependencia cambia
useEffect(() => {
console.log('usuarioId cambió a:', usuarioId);
// Ideal para: recargar datos cuando un prop/estado cambia
}, [usuarioId]);
// La regla: incluye en el array todo valor del componente
// que uses dentro del efecto (props, estado, funciones)
// Si lo omites, el efecto puede usar valores "rancios" (stale closure)
Ejemplo 1: cargar datos de una API
import { useState, useEffect } from 'react';
function PerfilUsuario({ usuarioId }) {
const [usuario, setUsuario] = useState(null);
const [cargando, setCargando] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// Resetear estado al cambiar de usuario
setCargando(true);
setError(null);
setUsuario(null);
// Variable para evitar actualizar el estado si el componente se desmontó
let cancelado = false;
async function cargarUsuario() {
try {
const respuesta = await fetch(`/api/usuarios/${usuarioId}`);
if (!respuesta.ok) {
throw new Error(`HTTP ${respuesta.status}`);
}
const datos = await respuesta.json();
if (!cancelado) {
setUsuario(datos);
}
} catch (err) {
if (!cancelado) {
setError(err.message);
}
} finally {
if (!cancelado) {
setCargando(false);
}
}
}
cargarUsuario();
// Función de limpieza: si el componente se desmonta o usuarioId cambia
// antes de que termine la petición, cancelamos la actualización del estado
return () => {
cancelado = true;
};
}, [usuarioId]); // se vuelve a ejecutar cada vez que usuarioId cambia
if (cargando) return <p>Cargando...</p>;
if (error) return <p>Error: {error}</p>;
if (!usuario) return null;
return (
<div>
<h2>{usuario.nombre}</h2>
<p>{usuario.email}</p>
</div>
);
}
Ejemplo 2: suscripción a un evento del DOM
import { useState, useEffect } from 'react';
function PosicionRaton() {
const [posicion, setPosicion] = useState({ x: 0, y: 0 });
useEffect(() => {
function actualizarPosicion(evento) {
setPosicion({ x: evento.clientX, y: evento.clientY });
}
// Suscribirse al evento al montar
window.addEventListener('mousemove', actualizarPosicion);
// Limpiar al desmontar: FUNDAMENTAL para evitar fugas de memoria
return () => {
window.removeEventListener('mousemove', actualizarPosicion);
};
}, []); // [] porque solo queremos suscribirnos una vez
return (
<p>Ratón en: X={posicion.x}, Y={posicion.y}</p>
);
}
Ejemplo 3: temporizador
import { useState, useEffect } from 'react';
function Cronometro() {
const [segundos, setSegundos] = useState(0);
const [activo, setActivo] = useState(false);
useEffect(() => {
if (!activo) return; // si no está activo, no hacer nada
const intervalo = setInterval(() => {
setSegundos(prev => prev + 1); // forma funcional: más segura en setInterval
}, 1000);
// Limpiar el intervalo cuando activo cambia o el componente se desmonta
return () => clearInterval(intervalo);
}, [activo]); // se vuelve a ejecutar cuando activo cambia
function formatearTiempo(seg) {
const m = Math.floor(seg / 60).toString().padStart(2, '0');
const s = (seg % 60).toString().padStart(2, '0');
return `${m}:${s}`;
}
return (
<div>
<p className="tiempo">{formatearTiempo(segundos)}</p>
<button onClick={() => setActivo(!activo)}>
{activo ? 'Pausar' : 'Iniciar'}
</button>
<button onClick={() => { setActivo(false); setSegundos(0); }}>
Resetear
</button>
</div>
);
}
Ejemplo 4: sincronizar con localStorage
import { useState, useEffect } from 'react'; function useLocalStorage(clave, valorInicial) { // Inicializar desde localStorage (lazy initialization) const [valor, setValor] = useState(() => { try { const guardado = localStorage.getItem(clave); return guardado ? JSON.parse(guardado) : valorInicial; } catch { return valorInicial; } }); // Sincronizar con localStorage cuando el valor cambia useEffect(() => { try { localStorage.setItem(clave, JSON.stringify(valor)); } catch { // localStorage puede fallar en modo privado o si está lleno } }, [clave, valor]); return [valor, setValor]; } // Usar el hook personalizado igual que useState function Preferencias() { const [tema, setTema] = useLocalStorage('tema', 'claro'); const [idioma, setIdioma] = useLocalStorage('idioma', 'es'); return ( <div> <select value={tema} onChange={e => setTema(e.target.value)}> <option value="claro">Claro</option> <option value="oscuro">Oscuro</option> </select> {/* Los cambios se guardan en localStorage automáticamente */} </div> ); }
Ejemplo 5: título del documento y cleanup
import { useState, useEffect } from 'react'; function Chat({ contacto }) { const [mensajes, setMensajes] = useState([]); const [noLeidos, setNoLeidos] = useState(0); // Actualizar el título de la pestaña con los mensajes no leídos useEffect(() => { if (noLeidos > 0) { document.title = `(${noLeidos}) Chat con ${contacto.nombre}`; } else { document.title = `Chat con ${contacto.nombre}`; } // Al desmontar el componente, restaurar el título original return () => { document.title = 'Mi aplicación'; }; }, [noLeidos, contacto.nombre]); // Suscribirse a mensajes nuevos (WebSocket simulado) useEffect(() => { const conexion = crearConexionChat(contacto.id); conexion.onMensaje = (mensaje) => { setMensajes(prev => [...prev, mensaje]); setNoLeidos(prev => prev + 1); }; conexion.conectar(); return () => { conexion.desconectar(); // cerrar la conexión al desmontar }; }, [contacto.id]); // nueva conexión si cambia el contacto return ( <div> <h2>{contacto.nombre} {noLeidos > 0 && <span>({noLeidos} nuevos)</span>}</h2> <div className="mensajes"> {mensajes.map((m, i) => <p key={i}>{m.texto}</p>)} </div> <button onClick={() => setNoLeidos(0)}>Marcar como leídos</button> </div> ); }
Los errores más comunes con useEffect
Dependencias faltantes (stale closure)
const [contador, setContador] = useState(0);
// ❌ contador no está en las dependencias: el efecto captura el valor inicial (0)
// y nunca lo actualiza aunque contador cambie en el estado
useEffect(() => {
const intervalo = setInterval(() => {
console.log('Contador:', contador); // siempre imprime 0
}, 1000);
return () => clearInterval(intervalo);
}, []); // ← falta contador aquí
// ✅ Opción 1: añadir contador a las dependencias
useEffect(() => {
const intervalo = setInterval(() => {
console.log('Contador:', contador);
}, 1000);
return () => clearInterval(intervalo);
}, [contador]); // se reinicia el intervalo cuando contador cambia
// ✅ Opción 2: usar la forma funcional del setter (no necesita leer el estado)
useEffect(() => {
const intervalo = setInterval(() => {
setContador(prev => {
console.log('Contador:', prev);
return prev; // no cambiamos el valor, solo lo leemos
});
}, 1000);
return () => clearInterval(intervalo);
}, []); // [] es correcto aquí porque no dependemos de contador directamente
Bucle infinito
// ❌ Bucle infinito: el efecto modifica un estado que está en sus dependencias const [datos, setDatos] = useState([]); useEffect(() => { setDatos([...datos, 'nuevo']); // modifica datos }, [datos]); // que está en las dependencias → bucle infinito // ✅ No incluir el estado que modificas como dependencia // Si el nuevo valor depende del anterior, usa la forma funcional useEffect(() => { setDatos(prev => [...prev, 'nuevo']); // no necesita leer datos }, []); // [] si solo quieres ejecutarlo una vez
Petición async directamente en useEffect
// ❌ No puedes hacer useEffect async directamente useEffect(async () => { // ← esto no funciona bien const datos = await fetch('/api'); setDatos(datos); }, []); // ✅ Define la función async dentro y llámala useEffect(() => { async function cargar() { const respuesta = await fetch('/api'); const datos = await respuesta.json(); setDatos(datos); } cargar(); }, []);
useState y useEffect juntos: el patrón completo de carga de datos
import { useState, useEffect } from 'react'; // Hook personalizado que encapsula el patrón completo function useDatos(url) { const [datos, setDatos] = useState(null); const [cargando, setCargando] = useState(true); const [error, setError] = useState(null); useEffect(() => { let cancelado = false; setCargando(true); setError(null); async function cargar() { try { const respuesta = await fetch(url); if (!respuesta.ok) throw new Error(`HTTP ${respuesta.status}`); const json = await respuesta.json(); if (!cancelado) setDatos(json); } catch (err) { if (!cancelado) setError(err.message); } finally { if (!cancelado) setCargando(false); } } cargar(); return () => { cancelado = true; }; }, [url]); return { datos, cargando, error }; } // Usar el hook en cualquier componente function ListaProductos() { const { datos, cargando, error } = useDatos('/api/productos'); if (cargando) return <p>Cargando productos...</p>; if (error) return <p>Error: {error}</p>; return ( <ul> {datos?.map(p => ( <li key={p.id}>{p.nombre} — {p.precio}€</li> ))} </ul> ); } function PerfilUsuario({ id }) { const { datos: usuario, cargando } = useDatos(`/api/usuarios/${id}`); if (cargando) return <p>Cargando...</p>; return <h2>{usuario?.nombre}</h2>; }
Resumen
useStateañade estado a un componente. Devuelve el valor actual y una función para actualizarlo. Cuando el estado cambia, React re-renderiza el componente.- Nunca mutes el estado directamente: crea siempre un nuevo valor. Para objetos usa spread (
{ ...obj, campo: valor }), para arrays usamap,filtero spread. - Usa la forma funcional del setter (
setValor(prev => prev + 1)) cuando el nuevo valor depende del anterior, especialmente dentro de temporizadores o suscripciones. useEffectsincroniza el componente con el mundo exterior: peticiones de red, eventos del DOM, temporizadores, suscripciones. Se ejecuta después del render.- El array de dependencias controla cuándo se ejecuta el efecto: sin array → cada render;
[]→ solo al montar;[dep]→ cuando dep cambia. - Devuelve una función de limpieza para cancelar suscripciones, limpiar temporizadores y evitar actualizaciones de estado en componentes desmontados.
- Para efectos asíncronos, define la función async dentro del efecto y llámala: no marques el callback de useEffect como async directamente.
- Extrae la lógica repetida de useState + useEffect en hooks personalizados: hacen el código reutilizable y los componentes más limpios.
No hay comentarios todavía. Sé el primero en compartir tu opinión.