El Ciclo de Vida de React Explicado: De Mounting a Unmounting
Entiende como nacen, viven y mueren los componentes de React. Una guía visual e interactiva sobre mounting, updating y unmounting desde cero.
El Ciclo de Vida de React: De Mounting a Unmounting
El ciclo de vida de React determina qué pasa cuando un componente aparece en pantalla, se actualiza con nuevos datos, y finalmente desaparece. Entender mounting, updating y unmounting es fundamental para saber cuando cargar datos, cuando limpiar recursos y cuando actualizar la interfaz.
Si estos términos te suenan confusos, esta guía te los explica con ejemplos concretos y código que puedes probar directamente.
La analogia de la tienda fisica
Para entender el ciclo de vida, piensa en una tienda fisica. 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 CUANDO hacer las cosas: cuando cargar datos, cuando limpiar recursos, cuando 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.
Piensalo así: cuando un componente se "monta", React esta 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.
Visualizalo
Mounting es como cuando nace un bebe. Antes no existia, ahora existe. Antes la pantalla estaba vacia (o tenia otra cosa), ahora tu componente esta ahi.
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.
Que causa un update en React?
Tres cosas principales:
a) Cambia el state (estado interno)
function Contador() {
const [número, setNumero] = useState(0)
// Cada vez que haces click, el componente se actualiza (re-renderiza)
return (
<div>
<p>Contador: {número}</p>
<button onClick={() => setNumero(número + 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 cambio. 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 esta disponible, las luces se apagan, todo se limpia.
Cuando se desmonta un componente?
a) Cambias de página/ruta
// Estas en la página de inicio
<HomePage /> // Montado
// Navegas a perfil
<ProfilePage /> // HomePage se desmonta, ProfilePage se montab) 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, cierrala
- Si pusiste un timer, cancelalo
- Si te suscribiste a eventos, desuscribete
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 especificos 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í esta como 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
Patron 1: Solo en mounting (una sola vez)
El caso más común es cargar datos con Fetch API cuando el componente aparece por primera vez:
function PerfilUsuario() {
const [usuario, setUsuario] = useState(null)
// Array vacio [] = solo se ejecuta una vez al montar
useEffect(() => {
console.log('Cargando usuario...')
fetch('/api/usuario')
.then(res => res.json())
.then(data => setUsuario(data))
}, []) // Array vacio es clave aquí
return <div>{usuario?.nombre}</div>
}cuándo usar: Cargar datos iniciales, configurar cosas una sola vez.
Analogia de la tienda: Cosas que haces solo cuando abres por primera vez en el día (encender luces, abrir caja registradora).
Patron 2: En mounting y cuando algo cambia
Un ejemplo típico es un buscador que reacciona cada vez que el usuario escribe (si necesitas búsqueda del lado del cliente, revisa como implementarla con Fuse.js):
function BuscadorProductos() {
const [búsqueda, setBusqueda] = useState('')
const [resultados, setResultados] = useState([])
// Se ejecuta cuando búsqueda cambia
useEffect(() => {
if (búsqueda.length > 0) {
console.log('Buscando:', búsqueda)
fetch(`/api/buscar?q=${búsqueda}`)
.then(res => res.json())
.then(data => setResultados(data))
}
}, [búsqueda]) // Se ejecuta cuando búsqueda cambia
return (
<div>
<input
value={búsqueda}
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 especificos (búsquedas, filtros, etc.).
Analogia de la tienda: Cada vez que llega un cliente nuevo (búsqueda cambia), le prestas atención (haces el fetch).
Patron 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.
Analogia 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 seguira ejecutandose 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í esta el código de cómo funciona:
[Boton: Mostrar Componente]
Estado: No montadoCuando haces click en "Mostrar Componente":
Estado: Montando...
Estado: Montado
[Componente visible aquí]
[Boton: Ocultar Componente]Cuando haces click en "Ocultar Componente":
Estado: Desmontando...
Estado: Desmontadocó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>
)
}Pruebalo:
- 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
[Boton: +1]
Renders totales: 1Cada vez que haces click:
Contador: 1
Renders totales: 2
Contador: 2
Renders totales: 3có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: veras 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 como funcionan estos cronometros:
Imagina un cronometro que se detiene automáticamente cuando el componente desaparece:
Cronometro: 5 segundos
[Boton: Ocultar cronometro]Si no limpias el intervalo, seguira corriendo en segundo plano incluso después de ocultar el componente.
código correcto:
function Cronometro() {
const [segundos, setSegundos] = useState(0)
useEffect(() => {
console.log('Cronometro iniciado')
const intervalo = setInterval(() => {
setSegundos(s => s + 1)
}, 1000)
// IMPORTANTE: Limpieza
return () => {
console.log('Cronometro 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 seguira ejecutandose eternamente, incluso después de que el componente desaparezca.
Casos de uso reales
1. Cargar datos al abrir una página
Este es el patron más frecuente: usar async/await o .then() dentro de useEffect para obtener datos del servidor.
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 (tip: válida la respuesta con Zod para asegurarte de que la estructura sea correcta)
- 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 tendrias 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 "Pantalon Negro"
- Props cambian, useEffect se ejecuta de nuevo
- título cambia a "Pantalon 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 como 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 vacioSin 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 esta 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 funcionara.
Corrección:
useEffect(() => {
fetch(`/api/buscar?q=${query}`)
.then(res => res.json())
.then(setResultados)
}, [query]) // Ahora si funcionaError 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 deberian 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 muertoResumen visual
| Etapa | Cuando ocurre | Que 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, incluyela en el array de dependencias. ESLint te ayudara con esto.
5. No optimices prematuramente
Empieza simple. React es eficiente por defecto. Optimiza solo si tienes problemas de rendimiento medibles.
Proximos pasos
Ahora que entiendes el ciclo de vida:
- práctica con los ejemplos de este artículo
- Abre la consola y observa cuando se ejecutan los efectos
- Crea un componente que cargue datos y limpielos correctamente
- Lee sobre
useEffectavanzado: dependencies, cleanup, custom hooks
Recursos adicionales
- Documentación oficial de useEffect -- referencia completa con ejemplos interactivos
- Referencia de Hooks de React -- todos los hooks disponibles (useState, useRef, useMemo, etc.)
- Serie completa: Learn React -- guía oficial para aprender React desde cero
Resumen
Con estos conceptos claros, ya sabes como y cuando ejecutar código en tus componentes. El ciclo de vida es la base para cargar datos correctamente, limpiar recursos y evitar memory leaks en tus aplicaciones React.
Preguntas frecuentes
¿Cuál es la diferencia entre los métodos de ciclo de vida de clase y los hooks?
Los métodos de ciclo de vida como componentDidMount y componentWillUnmount solo funcionan en componentes de clase. Los hooks como useEffect reemplazan estos métodos en componentes funcionales, ofreciendo una forma más simple y composable de manejar efectos secundarios.
¿Cómo funciona la limpieza (cleanup) en useEffect?
La función que retornas dentro de useEffect se ejecuta cuando el componente se desmonta o antes de que el efecto se vuelva a ejecutar. Es fundamental para limpiar timers con clearInterval, cerrar conexiones WebSocket, o remover event listeners y así evitar fugas de memoria.
¿Cuándo usar useEffect con array vacio vs con dependencias?
Usa useEffect con array vacio [] cuando quieras ejecutar código solo una vez al montar el componente, como cargar datos iniciales. Usa dependencias [variable] cuando necesites reaccionar a cambios especificos, como actualizar resultados de búsqueda cuando cambia el query.
Articulos relacionados
Zod Avanzado: Discriminated Unions, Transforms y Pipes
Patrones avanzados de Zod: discriminated unions, transforms, pipes, preprocess, y como validar datos complejos en TypeScript con schemas reutilizables.
tRPC + Next.js: APIs Type-Safe sin REST
Implementa tRPC en Next.js para APIs 100% type-safe. Sin schemas de API, sin fetch manual, sin types duplicados. End-to-end type safety con TypeScript.
Webhooks en Next.js: Recibe y Procesa Eventos
Implementa webhooks en Next.js para recibir eventos de Stripe, GitHub, Clerk y otros servicios. Verificación de firmas, tipado y manejo de errores.