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 el árbol de React generado durante la primera renderización en el cliente (hydration). React detecta una inconsistencia crítica y detiene la hidratación para evitar comportamientos impredecibles.
🔍 Causa raíz (en tu caso específico)
Dado que mencionas "Latest Version 16.2.11" y el contexto de App Router, es muy probable que estés usando Date() o new Date() directamente en el render de un componente, o alguna dependencia (como una librería de UI o un hook personalizado) que lo haga.
Ejemplo típico:
// ❌ Mala práctica: Date() se evalúa en SSR y en CSR, pero con microdiferencias de tiempo
function Timestamp() {
return <time>{new Date().toISOString()}</time>
}
Otra causa común: iOS Safari modifica el DOM inyectando enlaces automáticamente en lo que detecta como números de teléfono o fechas, lo cual rompe la consistencia entre SSR y CSR.
✅ Solución definitiva (pasos)
Paso 1: Aísla la fuente del desajuste
Busca en tu código (especialmente en app/, layout.tsx, page.tsx, componentes reutilizables) cualquier uso de:
-
new Date(),Date.now(),Math.random(),window,localStorage,navigator, etc. - Librerías que usan APIs del navegador sin protección (ej.
framer-motion,react-iconssinssr: false)
Paso 2: Aplica la solución correcta según el caso
🔹 Caso A: Contenido dinámico intencionalmente diferente (ej. timestamps, UUIDs)
Usa suppressHydrationWarning solo en el elemento afectado:
// ✅ Correcto: suprime advertencia *solo* en el elemento problemático
<time
datetime={new Date().toISOString()}
suppressHydrationWarning
>
{new Date().toLocaleString()}
</time>
⚠️ Importante: No envuelvas el componente padre en
suppressHydrationWarning, solo el elemento con contenido inestable.
🔹 Caso B: Uso de APIs del navegador (ej. window, localStorage)
Mueve la lógica al useEffect o usa dynamic con ssr: false:
// ✅ Opción 1: Con `useEffect`
function ClientOnlyTimestamp() {
const [date, setDate] = useState<string>('')
useEffect(() => {
setDate(new Date().toLocaleString())
}, [])
return <time suppressHydrationWarning>{date}</time>
}
// ✅ Opción 2: Con `dynamic` (recomendado para componentes completos)
import dynamic from 'next/dynamic'
const ClientOnlyComponent = dynamic(() => import('./ClientOnlyComponent'), {
ssr: false,
loading: () => <span>Cargando...</span>
})
export default function Page() {
return <ClientOnlyComponent />
}
🔹 Caso C: iOS Safari inyecta enlaces en fechas/teléfonos
Agrega esta etiqueta <meta> en tu 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>
)
}
Paso 3: Verifica configuraciones de infraestructura
- Cloudflare Auto Minify: Desactívalo temporalmente para descartar que el minificador de HTML esté alterando el markup.
-
CDNs o proxies intermedios: Algunos inyectan scripts o modifican atributos
data-*oclass.
🛠 Bloque de código corregido (ejemplo real)
// app/page.tsx
import { useState, useEffect } from 'react'
export default function Page() {
const [currentTime, setCurrentTime] = useState<string>('')
useEffect(() => {
setCurrentTime(new Date().toLocaleTimeString('es-ES', {
hour: '2-digit',
minute: '2-digit',
}))
}, [])
return (
<main className="p-4">
<h1>Última actualización</h1>
<p>
Hora del servidor (estática): 14:30
</p>
<p>
Hora del cliente (dinámica):
<time suppressHydrationWarning>{currentTime}</time>
</p>
</main>
)
}
💡 Pro-tip: Prevención a largo plazo
-
Usa linter con reglas de React: Instala
eslint-plugin-react-hooksyeslint-plugin-jsx-a11y. Detectan usos peligrosos deDate()owindowen render. -
Prueba en modo desarrollo: El error aparece en consola con
React hydration error, pero no aparece en producción (solo silenciosamente falla). Siempre desarrolla connext dev. -
Evita lógica de render condicional basada en
typeof window: UsauseEffectodynamic({ ssr: false })en su lugar.
✅ Verificación final: Si el error desaparece al deshabilitar JavaScript, el problema es de hidratación. Si persiste, revisa tu HTML generado (View Source vs DevTools Elements).
Top comments (0)