Efectos con useEffect
Con useState ya puedes hacer que tus componentes tengan memoria. Pero hay cosas que un componente necesita hacer que no son simplemente "mostrar datos": llamar a una API, cambiar el título de la página, configurar un temporizador, escuchar un evento del navegador. Estas operaciones se llaman efectos secundarios (side effects), y para ellas existe useEffect.
Piénsalo así: el componente tiene un trabajo principal (renderizar JSX) y trabajos secundarios (todo lo demás). useEffect es donde pones esos trabajos secundarios.
Sintaxis básica
import { useEffect } from 'react'
useEffect(() => {
// Este código se ejecuta después de que React
// renderiza el componente en pantalla
console.log('El componente se ha renderizado')
})
useEffect recibe una función que se ejecuta después de cada renderizado del componente. Pero normalmente no quieres que se ejecute siempre — quieres controlarlo. Para eso está el segundo argumento: el array de dependencias.
El array de dependencias
El segundo argumento de useEffect controla cuándo se ejecuta el efecto:
// 1. Sin array: se ejecuta después de CADA renderizado
useEffect(() => {
console.log('Cada renderizado')
})
// 2. Array vacío: se ejecuta solo UNA vez (al montarse)
useEffect(() => {
console.log('Solo al montar el componente')
}, [])
// 3. Con dependencias: se ejecuta al montar Y cuando cambian las dependencias
useEffect(() => {
console.log('El nombre ha cambiado:', nombre)
}, [nombre])
El array vacío [] es el más común cuando quieres hacer algo solo una vez — como llamar a una API cuando el componente aparece en pantalla.
Ejemplo: cambiar el título de la página
import { useState, useEffect } from 'react'
function Contador() {
const [cuenta, setCuenta] = useState(0)
// Cada vez que cambia "cuenta", actualiza el título de la pestaña
useEffect(() => {
document.title = `Café Estelar (${cuenta} pedidos)`
}, [cuenta])
return (
<div>
<p>Pedidos: {cuenta}</p>
<button onClick={() => setCuenta(prev => prev + 1)}>
Nuevo pedido
</button>
</div>
)
}
export default Contador
Cada vez que cuenta cambia, React vuelve a ejecutar el efecto y el título de la pestaña del navegador se actualiza. Fíjate en que [cuenta] le dice a React: "solo vuelve a ejecutar este efecto cuando cuenta cambie".
Ejemplo: ejecutar código al montar
Un patrón muy habitual es cargar datos cuando el componente aparece por primera vez:
import { useState, useEffect } from 'react'
function MensajeBienvenida() {
const [hora, setHora] = useState('')
useEffect(() => {
// Se ejecuta solo una vez, al montar el componente
const horaActual = new Date().getHours()
if (horaActual < 12) {
setHora('Buenos días')
} else if (horaActual < 20) {
setHora('Buenas tardes')
} else {
setHora('Buenas noches')
}
}, []) // ← Array vacío = solo al montar
return <h2>{hora}, bienvenido a Café Estelar</h2>
}
export default MensajeBienvenida
Cleanup: limpiar al desmontar
Algunos efectos necesitan "limpiarse" cuando el componente se desmonta o antes de ejecutarse de nuevo. Por ejemplo: temporizadores, suscripciones a eventos, o conexiones WebSocket. Para esto, el efecto devuelve una función de limpieza (cleanup).
import { useState, useEffect } from 'react'
function Reloj() {
const [hora, setHora] = useState(new Date())
useEffect(() => {
// Configurar el intervalo
const intervalo = setInterval(() => {
setHora(new Date())
}, 1000)
// Función de limpieza: se ejecuta al desmontar
return () => {
clearInterval(intervalo)
}
}, []) // Solo al montar
return (
<p>
Hora actual: {hora.toLocaleTimeString('es-ES')}
</p>
)
}
export default Reloj
Sin la función de limpieza, cada vez que el componente se desmonta y se vuelve a montar, se crearía un nuevo intervalo sin eliminar el anterior. Después de unas cuantas veces, tendrías docenas de intervalos ejecutándose simultáneamente — un memory leak.
Ejemplo: escuchar eventos del navegador
import { useState, useEffect } from 'react'
function AnchoVentana() {
const [ancho, setAncho] = useState(window.innerWidth)
useEffect(() => {
function handleResize() {
setAncho(window.innerWidth)
}
// Añadir listener
window.addEventListener('resize', handleResize)
// Cleanup: quitar listener al desmontar
return () => {
window.removeEventListener('resize', handleResize)
}
}, [])
return <p>Ancho de la ventana: {ancho}px</p>
}
export default AnchoVentana
Efecto con dependencias que cambian
Cuando el array de dependencias tiene valores que cambian, React ejecuta la limpieza del efecto anterior antes de ejecutar el nuevo:
import { useState, useEffect } from 'react'
function Buscador() {
const [termino, setTermino] = useState('')
const [resultados, setResultados] = useState([])
useEffect(() => {
// No buscar si el término está vacío
if (!termino.trim()) {
setResultados([])
return
}
// Simular una búsqueda con un timeout
const timeout = setTimeout(() => {
console.log('Buscando:', termino)
// Aquí irían los resultados reales de una API
setResultados([`Resultado para "${termino}"`])
}, 500)
// Cleanup: cancela el timeout anterior si el usuario sigue escribiendo
return () => clearTimeout(timeout)
}, [termino]) // Se ejecuta cada vez que cambia "termino"
return (
<div>
<input
value={termino}
onChange={(e) => setTermino(e.target.value)}
placeholder="Buscar café..."
/>
<ul>
{resultados.map((r, i) => <li key={i}>{r}</li>)}
</ul>
</div>
)
}
export default Buscador
Este patrón se llama debouncing: esperar a que el usuario deje de escribir antes de ejecutar la búsqueda. El cleanup cancela el timeout anterior cada vez que el usuario pulsa una tecla, y el timeout solo se completa cuando el usuario para de escribir durante 500ms.
Cuándo NO usar useEffect
Un error muy común en React es abusar de useEffect. No lo necesitas para:
// ❌ NO: calcular estado derivado con useEffect
const [items, setItems] = useState([...])
const [total, setTotal] = useState(0)
useEffect(() => {
setTotal(items.reduce((sum, item) => sum + item.precio, 0))
}, [items])
// ✅ SÍ: calcúlalo directamente (estado derivado)
const [items, setItems] = useState([...])
const total = items.reduce((sum, item) => sum + item.precio, 0)
// ❌ NO: transformar datos durante el render con useEffect
useEffect(() => {
setNombreMayusculas(nombre.toUpperCase())
}, [nombre])
// ✅ SÍ: calcúlalo directamente
const nombreMayusculas = nombre.toUpperCase()
La regla es simple: si puedes calcularlo durante el render, no uses useEffect. Reserva useEffect para operaciones que interactúan con el mundo exterior: APIs, DOM, temporizadores, localStorage, eventos del navegador.
Resumen
useEffect(fn, deps)ejecuta código después del renderizado, controlado por el array de dependencias.- Array vacío
[]: solo al montar. Sin array: cada render. Con valores: cuando esos valores cambian. - La función de cleanup (el return del efecto) se ejecuta al desmontar o antes de re-ejecutar el efecto.
- Usa cleanup para limpiar temporizadores, listeners y suscripciones.
- No abuses de useEffect: si puedes calcular un valor durante el render, hazlo directamente.
- Usa useEffect para operaciones con el mundo exterior: APIs, DOM, localStorage, eventos del navegador.
En la siguiente lección aprenderás a manejar eventos y formularios en React — cómo capturar datos del usuario, validar inputs y construir formularios controlados completos.
Reloj y guardado automático
Crea un componente NotasEstelar que combine varios efectos:
- Un
textareadonde el usuario escribe notas. Guarda automáticamente el contenido enlocalStoragecon un debounce de 1 segundo (usauseEffectcon cleanup desetTimeout). - Al montar el componente, carga las notas guardadas de
localStorage(si existen). - Muestra un reloj en tiempo real debajo del textarea (usando
setIntervalcon su cleanup correspondiente). - Cambia el título de la pestaña a "Notas Estelar — [número] caracteres" cada vez que el texto cambie.
- Muestra un indicador "Guardando..." que aparece cuando el usuario escribe y desaparece después del guardado.
lightbulb Pistas
Necesitarás tres useEffect separados: uno para el reloj (con setInterval y cleanup), otro para el debounce de guardado (con setTimeout y cleanup), y otro para el título de la pestaña. Para el indicador "Guardando...", usa un estado booleano que se active cuando el usuario escribe y se desactive dentro del setTimeout cuando el guardado se completa.