Cómo solucionar "Text content does not match server-rendered HTML" en Next.js App Router
Este error ocurre cuando el HTML generado en el servidor no coincide con el que React genera durante la hidratación inicial en el cliente. Es un problema crítico de consistencia de estado que rompe la hidratación y puede causar comportamientos impredecibles o UI rota.
Causa raíz
En tu caso, el problema está relacionado con contenido dinámico que varía entre renderizado del servidor y renderizado del cliente, probablemente causado por:
- Uso de
Date()onew Date()en el renderizado (ej. mostrar la hora actual, fechas relativas como "hace 5 minutos") - Uso de APIs del navegador (
window,localStorage,navigator) directamente en el render - Metaetiquetas o scripts que modifican el DOM antes de la hidratación (como iOS detectando números telefónicos y convirtiéndolos en enlaces)
- Extensiones del navegador inyectando contenido
- Configuraciones de CDN (ej. Cloudflare Auto Minify) que alteran el HTML prerrendereado
Solución definitiva (pasos verificados)
✅ Paso 1: Identificar el elemento problemático
Busca en tu código componentes que:
- Impriman fechas/horas actuales (
new Date(),Date.now()) - Usen
localStorage.getItem(),window.innerWidth, etc. - Rendericen contenido condicional basado en
typeof window !== 'undefined'
⚠️ Ejemplo común problemático:
// ❌ MAL: Rompe SSR porque `new Date()` da distinto valor en servidor vs cliente <footer>Última actualización: {new Date().toLocaleString()}</footer>
✅ Paso 2: Aplicar una de estas correcciones según el caso
🔹 Caso A: Contenido que debe variar (ej. timestamp)
Usa suppressHydrationWarning en el elemento específico:
<time
dateTime={new Date().toISOString()}
suppressHydrationWarning
>
{new Date().toLocaleString()}
</time>
✅ Pro-tip: Solo aplica esto a elementos textuales y de bajo impacto visual. Nunca en contenedores estructurales.
🔹 Caso B: Lógica dependiente del cliente (ej. localStorage)
Usa useEffect para retrasar la renderización:
'use client'
import { useState, useEffect } from 'react'
export default function UserGreeting() {
const [user, setUser] = useState<string | null>(null)
const [isClient, setIsClient] = useState(false)
useEffect(() => {
const storedUser = localStorage.getItem('user')
setUser(storedUser)
setIsClient(true)
}, [])
// Renderiza placeholder durante SSR y primera hidratación
if (!isClient) return <span>Cargando...</span>
return (
<div>
{user ? `Hola, ${user}` : 'Invitado'}
</div>
)
}
🔹 Caso C: iOS detecta números como teléfonos
Agrega la metaetiqueta en 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>
)
}
🔹 Caso D: Componente complejo con múltiples dependencias del cliente
Deshabilita SSR para ese componente específico:
// components/DynamicComponent.tsx
export default function DynamicComponent() {
// Aquí puedes usar window, localStorage, etc.
const [data, setData] = useState(null)
useEffect(() => {
setData(window.innerWidth)
}, [])
return <div>Ancho: {data}</div>
}
// En tu página
'use client'
import dynamic from 'next/dynamic'
const DynamicComponentNoSSR = dynamic(() => import('../components/DynamicComponent'), {
ssr: false,
})
export default function Page() {
return <DynamicComponentNoSSR />
}
✅ Paso 3: Verifica configuraciones de infraestructura
- Cloudflare: Desactiva Auto Minify en Speed > Optimization
- Otros CDN: Revisa si hay HTML rewriting o edge functions que modifiquen el HTML
- Extensiones de navegador: Prueba en modo incógnito para descartar interferencia
Pro-tip: Diagnóstico rápido
- Abre DevTools → Network tab → Desactiva Disable cache
- Recarga con Disable JavaScript activado → Si el HTML se ve correcto, el problema está en JS
- Usa
console.log('client', window)dentro de un componente → Si aparece en SSR, estás usando APIs del cliente en el render
🔥 Regla de oro: Todo lo que se renderice en el servidor debe producir el mismo HTML que el primer render del cliente. Si no puedes garantizarlo, usa
suppressHydrationWarningodynamic({ ssr: false }).
Aplica estos pasos y el error desaparecerá de forma permanente.
Top comments (0)