El Ciclo de Vida de React: De Mounting a Unmounting
Publicado el 30 de septiembre, 2025 • 12 min de lectura
¿Alguna vez te preguntaste qué pasa cuando abres una aplicación React? ¿Por qué algunos componentes "recuerdan" cosas y otros no? ¿Qué significa exactamente que un componente se "monte" o se "desmonte"?
Si estás empezando con React, estos términos pueden sonar confusos. Pero hoy vas a entender el ciclo de vida de los componentes de una forma simple y visual.
La analogía de la tienda física
Imagina que tienes una tienda física. Cada día pasa por tres etapas:
1. Abrir la tienda (Mounting)
- Llegas por la mañana
- Prendes las luces
- Organizas los productos
- Abres la puerta al público
2. Operar durante el día (Updating)
- Los clientes entran y salen
- Reorganizas productos
- Actualizas precios
- Respondes preguntas
3. Cerrar la tienda (Unmounting)
- Sacas a los últimos clientes
- Apagas las luces
- Guardas el dinero
- Cierras con llave
Un componente de React funciona exactamente igual. Nace (mounting), vive y cambia (updating), y eventualmente muere (unmounting).
¿Por qué es importante?
Entender el ciclo de vida te ayuda a saber CUÁNDO hacer las cosas: cuándo cargar datos, cuándo limpiar recursos, cuándo actualizar la pantalla. Es fundamental para escribir aplicaciones React que funcionen correctamente.
Las tres etapas del ciclo de vida
1. Mounting: Naciendo en la pantalla
Mounting es cuando tu componente aparece por primera vez en la pantalla.
Piénsalo así: cuando un componente se "monta", React está haciendo esto:
- Lee tu código del componente
- Ejecuta todo el código dentro del componente
- Crea los elementos HTML
- Los inserta en la página
- El usuario finalmente los ve
Ejemplo de la vida real:
Cuando abres Instagram, el componente del feed se monta:
- React carga las publicaciones
- Crea todos los elementos (imágenes, likes, comentarios)
- Los muestra en tu pantalla
function Feed() {
// Esto se ejecuta cuando el componente nace (se monta)
console.log('El Feed acaba de nacer')
return (
<div>
<h1>Tu Feed</h1>
<Post />
<Post />
<Post />
</div>
)
}
El console.log
se ejecuta una sola vez cuando el componente aparece por primera vez.
Visualízalo
Mounting es como cuando nace un bebé. Antes no existía, ahora existe. Antes la pantalla estaba vacía (o tenía otra cosa), ahora tu componente está ahí.
2. Updating: Cambiando mientras vive
Updating es cuando tu componente ya existe en la pantalla pero algo cambia.
Volviendo a la tienda: durante el día cambias precios, reorganizas productos, pero la tienda sigue abierta. No la cierras y vuelves a abrir, solo haces ajustes.
¿Qué causa un update en React?
Tres cosas principales:
a) Cambia el state (estado interno)
function Contador() {
const [numero, setNumero] = useState(0)
// Cada vez que haces click, el componente se actualiza (re-renderiza)
return (
<div>
<p>Contador: {numero}</p>
<button onClick={() => setNumero(numero + 1)}>
Sumar
</button>
</div>
)
}
Cada click causa un update. El componente no nace de nuevo, solo se actualiza para mostrar el nuevo número.
b) Cambian las props (datos que recibe de afuera)
function SaludoPersonalizado({ nombre }) {
// Si nombre cambia de "Ana" a "Carlos", esto se actualiza
return <h1>Hola, {nombre}</h1>
}
Si el componente padre cambia el nombre que le pasa, este componente se actualiza.
c) El componente padre se actualiza
function Padre() {
const [contador, setContador] = useState(0)
return (
<div>
<button onClick={() => setContador(contador + 1)}>
Incrementar
</button>
<Hijo /> {/* Este hijo se re-renderiza aunque no use el contador */}
</div>
)
}
Cuando el padre se actualiza, sus hijos también se actualizan (aunque hay formas de evitar esto).
Re-render no significa recrear todo
Cuando un componente se actualiza (re-renderiza), React NO borra todo y lo vuelve a crear. Solo actualiza lo que cambió. Es eficiente.
3. Unmounting: Desapareciendo de la pantalla
Unmounting es cuando tu componente se va, desaparece, deja de existir.
Es como cerrar la tienda al final del día. Ya no está disponible, las luces se apagan, todo se limpia.
¿Cuándo se desmonta un componente?
a) Cambias de página/ruta
// Estás en la página de inicio
<HomePage /> // Montado
// Navegas a perfil
<ProfilePage /> // HomePage se desmonta, ProfilePage se monta
b) Rendering condicional
function App() {
const [mostrarModal, setMostrarModal] = useState(false)
return (
<div>
<button onClick={() => setMostrarModal(true)}>
Abrir Modal
</button>
{mostrarModal && <Modal />} {/* Se monta cuando mostrarModal es true */}
<button onClick={() => setMostrarModal(false)}>
Cerrar Modal
</button>
{/* Al cerrar, Modal se desmonta (desaparece) */}
</div>
)
}
c) El componente padre se desmonta
Si un componente desaparece, todos sus hijos también desaparecen.
¿Por qué importa el unmounting?
Porque necesitas limpiar cosas cuando un componente se va:
- Si abriste una conexión a WebSocket, ciérrala
- Si pusiste un timer, cancélalo
- Si te suscribiste a eventos, desuscríbete
Si no limpias, causas memory leaks (fugas de memoria) y tu aplicación se vuelve lenta.
useEffect: El hook del ciclo de vida
useEffect
es la forma moderna de ejecutar código en momentos específicos del ciclo de vida.
Piensa en useEffect
como instrucciones especiales que le das a React:
"Oye React, cuando este componente se monte (o cuando algo específico cambie), ejecuta este código. Y si hay que limpiar algo cuando se desmonte, aquí está cómo hacerlo."
Estructura básica de useEffect
useEffect(() => {
// Código que se ejecuta después del mounting o updating
return () => {
// Código de limpieza que se ejecuta en unmounting
}
}, [dependencias])
Vamos a descomponerlo:
1. La función principal
useEffect(() => {
console.log('Componente montado o actualizado')
})
Este código se ejecuta:
- Después del primer render (mounting)
- Después de cada update (re-render)
2. El array de dependencias
useEffect(() => {
console.log('Solo cuando algo específico cambia')
}, [variable1, variable2])
Con el array de dependencias, el efecto solo se ejecuta cuando esas variables cambian.
3. La función de limpieza (cleanup)
useEffect(() => {
console.log('Componente montado')
return () => {
console.log('Componente desmontado - limpiando')
}
}, [])
La función que retornas se ejecuta justo antes de que el componente se desmonte.
Los tres patrones principales de useEffect
Patrón 1: Solo en mounting (una sola vez)
function PerfilUsuario() {
const [usuario, setUsuario] = useState(null)
// Array vacío [] = solo se ejecuta una vez al montar
useEffect(() => {
console.log('Cargando usuario...')
fetch('/api/usuario')
.then(res => res.json())
.then(data => setUsuario(data))
}, []) // Array vacío es clave aquí
return <div>{usuario?.nombre}</div>
}
Cuándo usar: Cargar datos iniciales, configurar cosas una sola vez.
Analogía de la tienda: Cosas que haces solo cuando abres por primera vez en el día (encender luces, abrir caja registradora).
Patrón 2: En mounting y cuando algo cambia
function BuscadorProductos() {
const [busqueda, setBusqueda] = useState('')
const [resultados, setResultados] = useState([])
// Se ejecuta cuando busqueda cambia
useEffect(() => {
if (busqueda.length > 0) {
console.log('Buscando:', busqueda)
fetch(`/api/buscar?q=${busqueda}`)
.then(res => res.json())
.then(data => setResultados(data))
}
}, [busqueda]) // Se ejecuta cuando busqueda cambia
return (
<div>
<input
value={busqueda}
onChange={(e) => setBusqueda(e.target.value)}
/>
<ul>
{resultados.map(r => <li key={r.id}>{r.nombre}</li>)}
</ul>
</div>
)
}
Cuándo usar: Reaccionar a cambios específicos (búsquedas, filtros, etc.).
Analogía de la tienda: Cada vez que llega un cliente nuevo (busqueda cambia), le prestas atención (haces el fetch).
Patrón 3: Con limpieza en unmounting
function RelojEnVivo() {
const [hora, setHora] = useState(new Date())
useEffect(() => {
console.log('Iniciando reloj')
// Actualizar la hora cada segundo
const intervalo = setInterval(() => {
setHora(new Date())
}, 1000)
// Limpieza: detener el intervalo cuando el componente se desmonte
return () => {
console.log('Deteniendo reloj')
clearInterval(intervalo)
}
}, [])
return <div>{hora.toLocaleTimeString()}</div>
}
Cuándo usar: Timers, suscripciones, conexiones que necesitas cerrar.
Analogía de la tienda: Cuando cierras al final del día, apagas las luces (clearInterval). Si no lo haces, las luces quedan prendidas toda la noche gastando electricidad (memory leak).
Error común: olvidar la limpieza
Si creas un interval, timer, o suscripción en useEffect, SIEMPRE limpia en el return. Si no, ese código seguirá ejecutándose incluso después de que el componente desaparezca.
Ejemplos visuales e interactivos
Demo 1: Mounting y Unmounting
¿Qué está pasando?
- • Click en "Mostrar" → El componente se monta (nace)
- • Click en "Ocultar" → El componente se desmonta (muere)
- • Revisa la consola del navegador para ver los logs
Demo 1: Visualizando mounting y unmounting
Arriba puedes ver el componente interactivo en acción. Aquí está el código de cómo funciona:
[Botón: Mostrar Componente]
Estado: No montado
Cuando haces click en "Mostrar Componente":
Estado: Montando...
Estado: Montado ✓
[Componente visible aquí]
[Botón: Ocultar Componente]
Cuando haces click en "Ocultar Componente":
Estado: Desmontando...
Estado: Desmontado
Código del demo:
function DemoMountingUnmounting() {
const [mostrar, setMostrar] = useState(false)
return (
<div>
<button onClick={() => setMostrar(!mostrar)}>
{mostrar ? 'Ocultar' : 'Mostrar'} Componente
</button>
{mostrar && <ComponenteDemo />}
</div>
)
}
function ComponenteDemo() {
useEffect(() => {
console.log('✅ Componente MONTADO')
return () => {
console.log('❌ Componente DESMONTADO')
}
}, [])
return (
<div style={{ padding: '20px', background: '#e0f7fa', margin: '10px' }}>
¡Hola! Estoy vivo en la pantalla
</div>
)
}
Pruébalo:
- Abre la consola del navegador (F12)
- Click en "Mostrar" - verás "✅ Componente MONTADO"
- Click en "Ocultar" - verás "❌ Componente DESMONTADO"
Demo 2: Visualizando updates (re-renders)
Contador: 0
[Botón: +1]
Renders totales: 1
Cada vez que haces click:
Contador: 1
Renders totales: 2
Contador: 2
Renders totales: 3
Código del demo:
function ContadorConRenders() {
const [contador, setContador] = useState(0)
const renderCount = useRef(0)
// Esto se ejecuta en cada render
renderCount.current = renderCount.current + 1
useEffect(() => {
console.log(`🔄 Update: Contador ahora es ${contador}`)
}, [contador])
return (
<div>
<h2>Contador: {contador}</h2>
<p>Renders totales: {renderCount.current}</p>
<button onClick={() => setContador(contador + 1)}>
+1
</button>
</div>
)
}
Observa: Cada click causa un update (re-render), pero el componente no se desmonta ni se vuelve a montar.
Demo 2: Updates (Re-renders)
¿Qué está pasando?
- • Cada click causa un update (re-render)
- • El componente NO se desmonta y vuelve a montar
- • Solo se actualiza el contenido que cambió
- • El contador de renders aumenta en cada actualización
- • Revisa la consola para ver los logs de cada update
Demo 3: Timer con limpieza
Demo 3: Limpieza con Cleanup Function
¿Qué está pasando?
- Cronómetro CORRECTO:
- • Usa
return () => clearInterval()
- • Cuando lo detienes, limpia el intervalo correctamente
- • No hay memory leaks
- • Usa
- Cronómetro INCORRECTO:
- • NO tiene función de limpieza
- • El intervalo sigue corriendo en background
- • Causa memory leaks (fuga de memoria)
🧪 Prueba esto:
- Abre la consola del navegador (F12)
- Inicia el cronómetro INCORRECTO
- Déjalo correr 3 segundos
- Deténlo con el botón
- Observa la consola: verás que sigue contando en background 😱
- Ahora prueba el cronómetro CORRECTO y verás que se detiene completamente
A continuación el código de cómo funcionan estos cronómetros:
Imagina un cronómetro que se detiene automáticamente cuando el componente desaparece:
Cronómetro: 5 segundos
[Botón: Ocultar cronómetro]
Si no limpias el intervalo, seguirá corriendo en segundo plano incluso después de ocultar el componente.
Código correcto:
function Cronometro() {
const [segundos, setSegundos] = useState(0)
useEffect(() => {
console.log('⏱️ Cronómetro iniciado')
const intervalo = setInterval(() => {
setSegundos(s => s + 1)
}, 1000)
// IMPORTANTE: Limpieza
return () => {
console.log('🛑 Cronómetro detenido')
clearInterval(intervalo)
}
}, [])
return <div>Tiempo: {segundos} segundos</div>
}
Sin limpieza (MAL):
function CronometroMalo() {
const [segundos, setSegundos] = useState(0)
useEffect(() => {
setInterval(() => {
setSegundos(s => s + 1)
}, 1000)
// Sin return = no limpia = memory leak
}, [])
return <div>Tiempo: {segundos} segundos</div>
}
El intervalo seguirá ejecutándose eternamente, incluso después de que el componente desaparezca.
Casos de uso reales
1. Cargar datos al abrir una página
function PaginaProductos() {
const [productos, setProductos] = useState([])
const [cargando, setCargando] = useState(true)
useEffect(() => {
// Cuando el componente se monta, carga los productos
fetch('/api/productos')
.then(res => res.json())
.then(data => {
setProductos(data)
setCargando(false)
})
}, []) // Solo al montar
if (cargando) {
return <div>Cargando productos...</div>
}
return (
<div>
{productos.map(p => (
<div key={p.id}>{p.nombre}</div>
))}
</div>
)
}
Ciclo de vida:
- Componente se monta
- useEffect ejecuta el fetch
- Mientras tanto, muestra "Cargando..."
- Cuando llegan los datos, actualiza el estado
- El componente se re-renderiza con los productos
2. Escuchar eventos del navegador
function DetectorTeclas() {
const [tecla, setTecla] = useState(null)
useEffect(() => {
// Función que se ejecuta cuando presionas una tecla
const manejarTecla = (evento) => {
setTecla(evento.key)
}
// Suscribirse al evento
window.addEventListener('keydown', manejarTecla)
// Limpieza: desuscribirse cuando el componente se desmonte
return () => {
window.removeEventListener('keydown', manejarTecla)
}
}, [])
return (
<div>
{tecla ? `Presionaste: ${tecla}` : 'Presiona cualquier tecla'}
</div>
)
}
Por qué es importante la limpieza: Si tienes 10 instancias de este componente que se montan y desmontan, sin limpieza tendrías 10 listeners activos consumiendo memoria.
3. Actualizar el título de la página
function PaginaProducto({ nombreProducto }) {
useEffect(() => {
// Actualizar el título del navegador
document.title = `${nombreProducto} - Mi Tienda`
// Limpieza: restaurar el título original
return () => {
document.title = 'Mi Tienda'
}
}, [nombreProducto]) // Se ejecuta cuando nombreProducto cambia
return <div>Viendo: {nombreProducto}</div>
}
Ciclo de vida:
- Componente se monta con "Camisa Azul"
- useEffect cambia el título a "Camisa Azul - Mi Tienda"
- Usuario navega a otro producto "Pantalón Negro"
- Props cambian, useEffect se ejecuta de nuevo
- Título cambia a "Pantalón Negro - Mi Tienda"
- Si el componente se desmonta, título vuelve a "Mi Tienda"
4. Guardar en localStorage
function FormularioConGuardado() {
const [nombre, setNombre] = useState('')
// Cargar desde localStorage al montar
useEffect(() => {
const guardado = localStorage.getItem('nombreFormulario')
if (guardado) {
setNombre(guardado)
}
}, [])
// Guardar en localStorage cada vez que nombre cambie
useEffect(() => {
localStorage.setItem('nombreFormulario', nombre)
}, [nombre])
return (
<input
value={nombre}
onChange={(e) => setNombre(e.target.value)}
placeholder="Tu nombre"
/>
)
}
Ciclo de vida:
- Componente se monta
- Primer useEffect carga el valor guardado
- Usuario escribe "A" → nombre cambia → segundo useEffect guarda
- Usuario escribe "n" → nombre cambia → segundo useEffect guarda
- Usuario recarga la página → primer useEffect carga "An"
Errores comunes y cómo evitarlos
Error 1: Olvidar el array de dependencias
// ❌ MAL: Se ejecuta en cada render
useEffect(() => {
console.log('Esto se ejecuta MUCHO')
fetch('/api/datos')
.then(...)
})
// ✓ BIEN: Solo al montar
useEffect(() => {
console.log('Esto se ejecuta una vez')
fetch('/api/datos')
.then(...)
}, []) // Array vacío
Sin el array de dependencias, el efecto se ejecuta después de cada render, causando requests infinitos.
Error 2: Dependencias incorrectas
function Buscador() {
const [query, setQuery] = useState('')
const [resultados, setResultados] = useState([])
// ❌ MAL: query está en el efecto pero no en dependencias
useEffect(() => {
fetch(`/api/buscar?q=${query}`)
.then(res => res.json())
.then(setResultados)
}, []) // Debería incluir [query]
return <input onChange={(e) => setQuery(e.target.value)} />
}
El efecto solo se ejecuta al montar, nunca cuando query cambia. La búsqueda no funcionará.
Corrección:
useEffect(() => {
fetch(`/api/buscar?q=${query}`)
.then(res => res.json())
.then(setResultados)
}, [query]) // Ahora sí funciona
Error 3: No limpiar suscripciones
// ❌ MAL: Memory leak
function Chat() {
useEffect(() => {
const socket = new WebSocket('ws://servidor.com')
socket.onmessage = (msg) => {
console.log(msg)
}
// Sin limpieza: la conexión queda abierta
}, [])
return <div>Chat</div>
}
// ✓ BIEN: Con limpieza
function Chat() {
useEffect(() => {
const socket = new WebSocket('ws://servidor.com')
socket.onmessage = (msg) => {
console.log(msg)
}
return () => {
socket.close() // Cerrar conexión al desmontar
}
}, [])
return <div>Chat</div>
}
Error 4: Múltiples efectos que deberían ser uno
// ❌ Confuso: Tres efectos relacionados
function PerfilUsuario({ userId }) {
const [usuario, setUsuario] = useState(null)
const [posts, setPosts] = useState([])
const [amigos, setAmigos] = useState([])
useEffect(() => {
fetch(`/api/usuario/${userId}`).then(...)
}, [userId])
useEffect(() => {
fetch(`/api/posts/${userId}`).then(...)
}, [userId])
useEffect(() => {
fetch(`/api/amigos/${userId}`).then(...)
}, [userId])
return <div>...</div>
}
// ✓ MEJOR: Un solo efecto
function PerfilUsuario({ userId }) {
const [usuario, setUsuario] = useState(null)
const [posts, setPosts] = useState([])
const [amigos, setAmigos] = useState([])
useEffect(() => {
// Cargar todo relacionado con el usuario
Promise.all([
fetch(`/api/usuario/${userId}`),
fetch(`/api/posts/${userId}`),
fetch(`/api/amigos/${userId}`)
]).then(([usuarioData, postsData, amigosData]) => {
setUsuario(usuarioData)
setPosts(postsData)
setAmigos(amigosData)
})
}, [userId])
return <div>...</div>
}
Error 5: Actualizar state en el momento equivocado
// ❌ MAL: setState directo en el cuerpo del componente
function Contador() {
const [count, setCount] = useState(0)
setCount(count + 1) // Esto causa un loop infinito
return <div>{count}</div>
}
// ✓ BIEN: setState en un evento o useEffect
function Contador() {
const [count, setCount] = useState(0)
const incrementar = () => {
setCount(count + 1)
}
return (
<div>
{count}
<button onClick={incrementar}>+1</button>
</div>
)
}
Diagrama del flujo completo
Aquí hay una visualización del ciclo de vida completo:
Usuario abre la app
↓
┌───────────────────────────────┐
│ MOUNTING │
│ - Constructor se ejecuta │
│ - useEffect sin deps [] │
│ - Componente aparece │
└───────────────────────────────┘
↓
┌───────────────────────────────┐
│ MONTADO (viviendo) │
│ - Esperando interacciones │
└───────────────────────────────┘
↓
¿Algo cambia?
(state, props, padre)
↓ SI
┌───────────────────────────────┐
│ UPDATING │
│ - Re-render │
│ - useEffect con deps │
│ - Pantalla se actualiza │
└───────────────────────────────┘
↓
Vuelve a estar montado
↓
¿Usuario se va?
¿Condición false?
↓ SI
┌───────────────────────────────┐
│ UNMOUNTING │
│ - Cleanup functions │
│ - return de useEffect │
│ - Componente desaparece │
└───────────────────────────────┘
↓
Componente muerto
Resumen visual
Etapa | Cuándo ocurre | Qué hacer aquí | Ejemplo |
---|---|---|---|
Mounting | Componente aparece por primera vez | Cargar datos, configurar cosas | useEffect(() => { fetch() }, []) |
Updating | State o props cambian | Reaccionar a cambios | useEffect(() => { }, [variable]) |
Unmounting | Componente desaparece | Limpiar: timers, eventos, conexiones | return () => { clearInterval() } |
Consejos finales
1. Piensa en términos de efectos, no de ciclo de vida
En lugar de pensar "necesito hacer esto en componentDidMount", piensa "necesito hacer esto cuando el componente se monta".
2. Un efecto por responsabilidad
No mezcles lógica no relacionada en un solo useEffect. Si cargas datos y también pones un timer, usa dos efectos separados.
3. Siempre limpia
Si creas algo que consume recursos (timers, conexiones, listeners), SIEMPRE limpia en el return.
4. Usa las dependencias correctamente
Si usas una variable dentro del efecto, inclúyela en el array de dependencias. ESLint te ayudará con esto.
5. No optimices prematuramente
Empieza simple. React es eficiente por defecto. Optimiza solo si tienes problemas de rendimiento medibles.
Próximos pasos
Ahora que entiendes el ciclo de vida:
- Practica con los ejemplos de este artículo
- Abre la consola y observa cuándo se ejecutan los efectos
- Crea un componente que cargue datos y límpielos correctamente
- Lee sobre
useEffect
avanzado: dependencies, cleanup, custom hooks
Recursos adicionales
- Documentación oficial de useEffect
- Visualizador de ciclo de vida interactivo
- Serie completa sobre Hooks
¡Felicidades!
Entender el ciclo de vida es un hito importante en tu camino con React. Ahora sabes cómo y cuándo ejecutar código en tus componentes. Esto es fundamental para crear aplicaciones que funcionen correctamente.
¿Te ayudó este artículo? Compártelo con alguien que esté aprendiendo React. Si tienes dudas o quieres ver más ejemplos, déjame un comentario.
Sobre el autor: Desarrollador especializado en React y NextJS. Creo contenido educativo en español para ayudar a la comunidad de desarrolladores en Latinoamérica.