Tema
useEffect y ciclo de vida
Algunas cosas no son "dibujar la interfaz": suscribirse a eventos de la ventana, iniciar un temporizador, sincronizar con localStorage o conectarse a un servicio externo. Para eso existe useEffect. Aprenderás cuándo se ejecuta, cómo funcionan las dependencias, la limpieza y, muy importante, cuándo no usarlo.
Concepto
Un componente debe ser puro: con las mismas props y estado devuelve el mismo JSX, sin tocar nada externo. Pero las apps necesitan efectos secundarios (side effects): hablar con el mundo exterior.
useEffect te permite ejecutar código después de que React pinta la pantalla, para sincronizar tu componente con algo externo.
jsx
import { useEffect, useState } from 'react'
function Titulo() {
const [clics, setClics] = useState(0)
useEffect(() => {
// Se ejecuta después de pintar, cada vez que "clics" cambia
document.title = `Clics: ${clics}`
}, [clics])
return <button onClick={() => setClics(clics + 1)}>Clic ({clics})</button>
}Ciclo de vida de un componente
txt
MONTAJE ACTUALIZACIÓN DESMONTAJE
(aparece) (cambian props/estado) (desaparece)
│ │ │
render render │
│ │ │
pinta DOM pinta DOM │
│ │ │
efecto ──────► limpieza anterior + efecto ───► limpieza finalEl arreglo de dependencias
El segundo argumento controla cuándo se vuelve a ejecutar el efecto:
jsx
// 1. Sin arreglo: después de CADA render (rara vez lo quieres)
useEffect(() => {
console.log('Cada render')
})
// 2. Arreglo vacío: sólo al montar (una vez)
useEffect(() => {
console.log('Sólo al aparecer')
}, [])
// 3. Con dependencias: al montar y cuando alguna cambie
useEffect(() => {
console.log(`El usuario cambió a ${usuarioId}`)
}, [usuarioId])Regla: incluye en las dependencias todo valor reactivo (props, estado o variables derivadas de ellos) que uses dentro del efecto. El plugin de ESLint react-hooks/exhaustive-deps (incluido en la plantilla de Vite) te avisa si falta alguno.
La función de limpieza
Si el efecto crea algo (un intervalo, un listener, una conexión), devuelve una función que lo deshaga. React la llama antes de volver a ejecutar el efecto y al desmontar el componente.
jsx
import { useEffect, useState } from 'react'
function Reloj() {
const [hora, setHora] = useState(() => new Date())
useEffect(() => {
const id = setInterval(() => setHora(new Date()), 1000)
// Limpieza: sin esto, el intervalo seguiría corriendo al desmontar
return () => clearInterval(id)
}, [])
return <p>{hora.toLocaleTimeString('es-MX')}</p>
}jsx
function TamañoVentana() {
const [ancho, setAncho] = useState(window.innerWidth)
useEffect(() => {
const manejar = () => setAncho(window.innerWidth)
window.addEventListener('resize', manejar)
return () => window.removeEventListener('resize', manejar)
}, [])
return <p>Ancho: {ancho}px {ancho < 768 ? '📱' : '🖥️'}</p>
}¿Por qué mi efecto se ejecuta dos veces?
En desarrollo, StrictMode monta, desmonta y vuelve a montar cada componente para ayudarte a detectar efectos sin limpieza. Si tu efecto funciona bien con esa secuencia, está bien escrito. En producción sólo se ejecuta una vez.
Casos de uso reales
Sincronizar con localStorage
jsx
import { useEffect, useState } from 'react'
function Notas() {
const [notas, setNotas] = useState(() => {
try {
return JSON.parse(localStorage.getItem('notas')) ?? ''
} catch {
return ''
}
})
// Cada vez que cambian las notas, se guardan
useEffect(() => {
localStorage.setItem('notas', JSON.stringify(notas))
}, [notas])
return <textarea value={notas} onChange={(e) => setNotas(e.target.value)} rows={8} cols={50} />
}Atajos de teclado
jsx
function Modal({ abierto, onCerrar, children }) {
useEffect(() => {
if (!abierto) return // no hagas nada si está cerrado
const manejarTecla = (e) => e.key === 'Escape' && onCerrar()
document.addEventListener('keydown', manejarTecla)
document.body.style.overflow = 'hidden' // bloquea el scroll de fondo
return () => {
document.removeEventListener('keydown', manejarTecla)
document.body.style.overflow = ''
}
}, [abierto, onCerrar])
if (!abierto) return null
return (
<div className="fondo-modal" onClick={onCerrar}>
<div className="modal" role="dialog" aria-modal="true" onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>
)
}Debounce de una búsqueda
Esperar a que el usuario deje de escribir antes de buscar:
jsx
import { useEffect, useState } from 'react'
function Buscador() {
const [texto, setTexto] = useState('')
const [termino, setTermino] = useState('')
useEffect(() => {
// Espera 400 ms; si el usuario sigue escribiendo, se cancela y reinicia
const id = setTimeout(() => setTermino(texto), 400)
return () => clearTimeout(id)
}, [texto])
return (
<>
<input value={texto} onChange={(e) => setTexto(e.target.value)} placeholder="Buscar..." />
<p>Buscando: <strong>{termino}</strong></p>
</>
)
}La petición de datos a una API con useEffect la verás a fondo en la siguiente lección.
Cuándo NO usar useEffect
Este es el error más común en React. Si algo se puede calcular durante el render o responder a un evento, no necesitas un efecto.
❌ Estado derivado con efecto
jsx
// ❌ Render extra y código innecesario
const [nombre, setNombre] = useState('Ana')
const [apellido, setApellido] = useState('López')
const [completo, setCompleto] = useState('')
useEffect(() => {
setCompleto(`${nombre} ${apellido}`)
}, [nombre, apellido])
// ✅ Calcúlalo en el render
const completo = `${nombre} ${apellido}`❌ Lógica de eventos en un efecto
jsx
// ❌ Enviar cuando cambia una bandera
useEffect(() => {
if (enviado) fetch('/api/pedido', { method: 'POST', body: JSON.stringify(carrito) })
}, [enviado])
// ✅ Hazlo directamente en el manejador del evento
function manejarCompra() {
fetch('/api/pedido', { method: 'POST', body: JSON.stringify(carrito) })
}❌ Reiniciar estado cuando cambia una prop
jsx
// ❌
useEffect(() => { setComentario('') }, [usuarioId])
// ✅ Usa key: React crea un componente nuevo con estado limpio
<PerfilUsuario key={usuarioId} usuarioId={usuarioId} />Pregúntate: "¿Esto sincroniza con algo externo a React?" Si la respuesta es no, probablemente no necesitas useEffect.
Error común: bucle infinito
jsx
useEffect(() => {
setContador(contador + 1) // cambia el estado → re-render → efecto → cambia el estado...
})Un efecto que actualiza un estado del que depende, sin condición, provoca un bucle infinito. Revisa las dependencias y la lógica.
Error común: objetos como dependencias
jsx
const opciones = { ciudad: 'CDMX' } // objeto nuevo en cada render
useEffect(() => { /* ... */ }, [opciones]) // se ejecuta en CADA renderUsa valores primitivos (opciones.ciudad) como dependencias o crea el objeto dentro del efecto.
Ejercicios
- Crea un componente
Cronometrocon botones Iniciar, Pausar y Reiniciar. UsauseEffectcon limpieza. - Crea un componente
EstadoConexionque muestre "🟢 En línea" o "🔴 Sin conexión" escuchando los eventosonlineyofflinedewindow.
Ver solución
jsx
// 1
import { useEffect, useState } from 'react'
export function Cronometro() {
const [segundos, setSegundos] = useState(0)
const [corriendo, setCorriendo] = useState(false)
useEffect(() => {
if (!corriendo) return
const id = setInterval(() => setSegundos((s) => s + 1), 1000)
return () => clearInterval(id)
}, [corriendo])
const mm = String(Math.floor(segundos / 60)).padStart(2, '0')
const ss = String(segundos % 60).padStart(2, '0')
return (
<div>
<h2>{mm}:{ss}</h2>
<button onClick={() => setCorriendo(true)} disabled={corriendo}>Iniciar</button>
<button onClick={() => setCorriendo(false)} disabled={!corriendo}>Pausar</button>
<button onClick={() => { setCorriendo(false); setSegundos(0) }}>Reiniciar</button>
</div>
)
}jsx
// 2
import { useEffect, useState } from 'react'
export function EstadoConexion() {
const [enLinea, setEnLinea] = useState(navigator.onLine)
useEffect(() => {
const online = () => setEnLinea(true)
const offline = () => setEnLinea(false)
window.addEventListener('online', online)
window.addEventListener('offline', offline)
return () => {
window.removeEventListener('online', online)
window.removeEventListener('offline', offline)
}
}, [])
return <p>{enLinea ? '🟢 En línea' : '🔴 Sin conexión'}</p>
}Pruébalo en DevTools → Network → "Offline".
Resumen
useEffect(fn, deps)ejecuta código después de pintar para sincronizar con sistemas externos.[]= sólo al montar;[a, b]= cuando cambianaob; sin arreglo = cada render.- Devuelve una función de limpieza para intervalos, listeners y suscripciones.
- No uses efectos para estado derivado ni para lógica de eventos.