Skip to content

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 final

El 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 render

Usa valores primitivos (opciones.ciudad) como dependencias o crea el objeto dentro del efecto.

Ejercicios ​

  1. Crea un componente Cronometro con botones Iniciar, Pausar y Reiniciar. Usa useEffect con limpieza.
  2. Crea un componente EstadoConexion que muestre "🟢 En línea" o "🔴 Sin conexión" escuchando los eventos online y offline de window.
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 cambian a o b; 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.

Academia Web — aprende, copia y construye.