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 (renderToString) no coincide con el árbol de React generado durante la primera renderización en el cliente (hydration). Es un fallo crítico de consistencia que rompe la experiencia de usuario y puede afectar el SEO.
Causa raíz
En tu caso, el problema está directamente relacionado con el uso de APIs sensibles al tiempo o contexto del cliente en el renderizado inicial, como Date(), new Date(), Date.now(), o incluso funciones que dependen de Date.now() para generar contenido dinámico (por ejemplo, fechas relativas como "hace 5 minutos").
El servidor genera un valor estático (ej. Mon Jun 09 2025 10:00:00 GMT+0000), pero al hidratarse en el cliente, el navegador genera un valor distinto (aunque sea milisegundos después), causando el mismatch.
Otras causas comunes en tu contexto:
- Uso de
typeof window !== 'undefined'directamente en el render. - Uso de
localStorage,sessionStorage,navigator, etc., sin protección. - Meta tags dinámicos que incluyen fechas/horas actuales.
- Extensiones del navegador (como Dark Reader) que modifican el DOM antes de la hidratación.
- Servidores proxy/CDN que minifican HTML (Cloudflare Auto Minify es un culpable frecuente).
Solución definitiva (pasos verificados en producción)
✅ Paso 1: Identifica la fuente exacta del mismatch
Busca en tu código:
- Cualquier uso directo de
Date(),new Date(),Date.now(),toLocaleString(),toISOString()en componentes que se renderizan en el<body>. - Fechas relativas calculadas en el render (ej.
new Date() - createdDate). - Componentes que renderizan
Date.now()operformance.now().
🔍 Comando útil para detectar sospechosos:
grep -r "new Date()" src/app
grep -r "Date.now()" src/app
grep -r "toLocaleString()" src/app
✅ Paso 2: Aplica la solución correcta según el caso
🟢 Caso A: Contenido que debe cambiar en el cliente (ej. timestamp actual)
Usa suppressHydrationWarning en el elemento específico:
// ✅ CORRECTO: Solo suprime el warning en el elemento problemático
<time
dateTime={new Date().toISOString()}
suppressHydrationWarning
>
{new Date().toLocaleString()}
</time>
⚠️ Importante: No lo uses en elementos contenedores (como <div>), solo en el nodo de texto exacto que varía.
🟢 Caso B: Lógica condicional basada en window/localStorage
Envuelve la lógica con useEffect + estado local:
'use client'
import { useState, useEffect } from 'react'
export default function ClientOnlyComponent() {
const [isClient, setIsClient] = useState(false)
const [user, setUser] = useState<string | null>(null)
useEffect(() => {
setIsClient(true)
setUser(localStorage.getItem('user')) // ✅ Seguro: solo se ejecuta en cliente
}, [])
if (!isClient) return <span>Cargando...</span> // ✅ Mismo HTML en SSR y primer client render
return <span>Hola, {user || 'invitado'}!</span>
}
🟢 Caso C: Componente que nunca debe SSR (ej. reloj en tiempo real, canvas)
Desactiva SSR con next/dynamic:
// components/RealTimeClock.tsx
'use client'
export default function RealTimeClock() {
const [time, setTime] = useState(new Date())
useEffect(() => {
const timer = setInterval(() => setTime(new Date()), 1000)
return () => clearInterval(timer)
}, [])
return <time>{time.toLocaleTimeString()}</time>
}
// page.tsx
import dynamic from 'next/dynamic'
const RealTimeClock = dynamic(() => import('./components/RealTimeClock'), {
ssr: false,
loading: () => <span>Cargando reloj...</span>
})
export default function Page() {
return <RealTimeClock />
}
✅ Paso 3: Verifica configuración de CDNs/Proxies
Si usas Cloudflare, Vercel Edge Config, o cualquier proxy que modifique HTML:
- Cloudflare: Desactiva Auto Minify (HTML) en Speed > Optimization.
-
Vercel: Asegúrate de no tener middleware que modifique
response.text()sin preservar el DOM.
Pro-Tip: Diagnóstico rápido en desarrollo
- Reproduce el error en modo estricto:
NEXT_STRICT_MODE=1 next dev
Esto activa doble renderizado en desarrollo y muestra el mismatch exacto en consola.
-
Inspecciona el HTML pre-hidratación:
- Abre DevTools > Network > marca Disable cache.
- Recarga la página con
Ctrl+Shift+R(oCmd+Shift+R). - Busca en el panel Elements el texto que difiere entre "Server" y "Client" (verás una marca amarilla en el DOM).
Herramienta de validación:
npx next telemetry disable
npx next build && npx next export
Si el error desaparece en export, el problema está en SSR dinámico (no en hidratación real).
Bonus: Solución para iOS (caso frecuente en apps móviles)
Si usas fechas o números que iOS convierte en enlaces automáticamente:
Agrega en app/layout.tsx:
<head>
<meta
name="format-detection"
content="telephone=no, date=no, email=no, address=no"
/>
</head>
Esto evita que Safari inyecte <a href="tel:..."> o <a href="mailto:..."> después del SSR, causando mismatches silenciosos.
✅ Resultado esperado: El error desaparece sin suprimir warnings innecesarios, y tu app mantiene SSR seguro y optimizado.
Top comments (0)