Blog

RA

Rod Alexanderson

Desarrollador Web

Creando documentación técnica en español para desarrolladores de Latinoamérica.

Más sobre mí →

Suscríbete al Newsletter

Recibe los nuevos artículos directamente en tu email.

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:

  1. Lee tu código del componente
  2. Ejecuta todo el código dentro del componente
  3. Crea los elementos HTML
  4. Los inserta en la página
  5. 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

Estado:⚫ No montado

¿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:

  1. Abre la consola del navegador (F12)
  2. Click en "Mostrar" - verás "✅ Componente MONTADO"
  3. 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)

Contador
0
Renders totales
1

¿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

✅ Con limpieza
❌ Sin limpieza (memory leak)

¿Qué está pasando?

  • Cronómetro CORRECTO:
    • • Usa return () => clearInterval()
    • • Cuando lo detienes, limpia el intervalo correctamente
    • • No hay memory leaks
  • Cronómetro INCORRECTO:
    • • NO tiene función de limpieza
    • • El intervalo sigue corriendo en background
    • • Causa memory leaks (fuga de memoria)

🧪 Prueba esto:

  1. Abre la consola del navegador (F12)
  2. Inicia el cronómetro INCORRECTO
  3. Déjalo correr 3 segundos
  4. Deténlo con el botón
  5. Observa la consola: verás que sigue contando en background 😱
  6. 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:

  1. Componente se monta
  2. useEffect ejecuta el fetch
  3. Mientras tanto, muestra "Cargando..."
  4. Cuando llegan los datos, actualiza el estado
  5. 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:

  1. Componente se monta con "Camisa Azul"
  2. useEffect cambia el título a "Camisa Azul - Mi Tienda"
  3. Usuario navega a otro producto "Pantalón Negro"
  4. Props cambian, useEffect se ejecuta de nuevo
  5. Título cambia a "Pantalón Negro - Mi Tienda"
  6. 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:

  1. Componente se monta
  2. Primer useEffect carga el valor guardado
  3. Usuario escribe "A" → nombre cambia → segundo useEffect guarda
  4. Usuario escribe "n" → nombre cambia → segundo useEffect guarda
  5. 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

EtapaCuándo ocurreQué hacer aquíEjemplo
MountingComponente aparece por primera vezCargar datos, configurar cosasuseEffect(() => { fetch() }, [])
UpdatingState o props cambianReaccionar a cambiosuseEffect(() => { }, [variable])
UnmountingComponente desapareceLimpiar: timers, eventos, conexionesreturn () => { 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:

  1. Practica con los ejemplos de este artículo
  2. Abre la consola y observa cuándo se ejecutan los efectos
  3. Crea un componente que cargue datos y límpielos correctamente
  4. Lee sobre useEffect avanzado: dependencies, cleanup, custom hooks

Recursos adicionales

¡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.