Cómo solucionar el error "Text content does not match server-rendered HTML" en Next.js App Router
Este error ocurre cuando el HTML generado en el servidor (SSR/SSG) no coincide con lo que React intenta renderizar en el navegador durante la hidratación. Es un problema crítico que rompe la experiencia de usuario y puede afectar el SEO.
¿Por qué ocurre?
En Next.js App Router, el servidor genera HTML estático que luego se "hidrata" en el cliente. Si el árbol de React en el navegador difiere en estructura o contenido del HTML inicial, React lanza este error. Las causas más comunes:
- Uso de APIs del navegador (
window,localStorage,Date.now()) en el renderizado inicial. - Lógica condicional basada en
typeof window !== 'undefined'dentro del render. - Componentes con estado inicial diferente entre servidor y cliente (ej.
useStatecon valor no determinista). - Metaetiquetas de detección automática en iOS (
format-detection) que inyectan enlaces<a>dinámicamente. - Minificación automática de CDN (Cloudflare, Vercel Edge) que altera el HTML prerrendereado.
Solución definitiva (pasos verificados)
✅ Paso 1: Identifica la fuente del desajuste
Busca en tu código:
-
Date(),new Date(),Math.random(),window,localStorage,navigator, etc. - Uso de
useStatecon valores no deterministas (ej.useState(() => Math.random())) - Componentes que renderizan contenido diferente según el entorno sin protección
🔍 Herramienta útil: Usa
console.log('client')dentro de componentes para ver si se ejecutan en SSR (no aparecerá en logs del servidor).
✅ Paso 2: Aplica la solución según el caso
Caso A: Contenido dinámico (fechas, IDs, tokens)
Solución recomendada: suppressHydrationWarning en el elemento específico.
// ❌ Mal: desajuste directo
<p>Última actualización: {new Date().toLocaleString()}</p>
// ✅ Bien: suprime advertencia en el elemento problemático
<p suppressHydrationWarning>
Última actualización: {new Date().toLocaleString()}
</p>
⚠️ Importante: Solo usa
suppressHydrationWarningen elementos textuales y de bajo impacto visual. Nunca en contenedores estructurales.
Caso B: Lógica condicional basada en entorno
Solución recomendada: Usa useEffect para diferir la lógica client-side.
'use client'
import { useState, useEffect } from 'react'
export default function ClientOnlyComponent() {
const [data, setData] = useState<string>('Cargando...')
useEffect(() => {
// Solo se ejecuta en el cliente
const stored = localStorage.getItem('user')
setData(stored || 'Invitado')
}, [])
return <div>Hola, {data}</div>
}
✅ Ventaja: El servidor renderiza
"Cargando...", y el cliente lo actualiza sin desajuste.
Caso C: Componente totalmente client-side (ej. gráficos, canvas)
Solución recomendada: Deshabilita SSR con next/dynamic.
// components/Chart.tsx
'use client'
import dynamic from 'next/dynamic'
const Chart = dynamic(() => import('./Chart'), { ssr: false })
export default function Page() {
return (
<div>
<h1>Dashboard</h1>
<Chart />
</div>
)
}
✅ Ventaja: El componente no se renderiza en el servidor → cero riesgo de desajuste.
✅ Paso 3: Solución global para iOS (frecuente en App Router)
Agrega esta metaetiqueta en <head> de tu layout principal (app/layout.tsx):
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="es">
<head>
<meta
name="format-detection"
content="telephone=no, date=no, email=no, address=no"
/>
</head>
<body>{children}</body>
</html>
)
}
📱 Por qué: iOS inyecta automáticamente
<a href="tel:...">en números de teléfono, rompiendo el HTML prerrendereado.
Pro-tip: Prevención definitiva
- Nunca uses APIs del navegador en el render top-level:
// ❌ Evitar
const time = new Date().toISOString()
return <time>{time}</time>
-
Valida siempre estructuras HTML:
-
<p>no puede contener<div>,<ul>,<button>, etc. -
<a>no puede contener otro<a>ni<button>.
-
-
En entornos de producción:
- Desactiva minificación automática de HTML en Cloudflare (o configura reglas para excluir
/app/**). - Usa
next.config.jspara deshabilitar optimizaciones problemáticas:
module.exports = { // Solo si es estrictamente necesario // experimental: { optimizePackageImports: true }, } - Desactiva minificación automática de HTML en Cloudflare (o configura reglas para excluir
Prueba en modo estricto:
npm run dev -- --turbo
# o
npm run build && npm run start
¿Sigues teniendo el error?
- Verifica extensiones del navegador: Desactiva todas y prueba en modo incógnito.
- Revisa tu CDN/Edge: Cloudflare, Fastly, o Vercel Edge Functions pueden alterar el HTML.
-
Usa
console.logen el servidor: Añadeconsole.log('Server render:', JSON.stringify(data))para comparar con el cliente.
🔥 Último recurso: Usa
suppressHydrationWarningen el<html>o<body>como medida de último momento — solo si entiendes los riesgos (pierdes validación de hidratación en toda la página).
Con esto, el error desaparecerá en <5 minutos. ¡A codear!
Top comments (0)