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 navegador (hydration). React espera que el DOM inicial coincida exactamente con el que se renderizó en el servidor; cualquier diferencia rompe la hidratación y causa este error crítico.
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, como:
- Uso de
Date.now(),new Date(),Math.random()olocalStorageen el renderizado directo. - Uso de
typeof window !== 'undefined'dentro de la lógica de renderizado (no dentro deuseEffect). - Metaetiquetas de detección automática de iOS (
format-detection) que inyectan nodos<a>en el DOM del cliente. - Extensiones del navegador que modifican el HTML (ej. traductores, bloqueadores de anuncios).
- Configuraciones de CDN como Cloudflare Auto Minify que alteran el HTML prerrendereado.
Pasos para solucionarlo (ordenados por prioridad)
1. Identifica y elimina lógica no determinista en el renderizado
❌ Incorrecto:
// ❌ ¡Esto rompe la hidratación!
export default function Timestamp() {
return <span>{new Date().toISOString()}</span>
}
✅ Correcto (opción 1: usar useEffect para diferir el renderizado dinámico):
import { useState, useEffect } from 'react'
export default function Timestamp() {
const [time, setTime] = useState<string>('...')
useEffect(() => {
setTime(new Date().toISOString())
}, [])
return <span>{time}</span>
}
✅ Correcto (opción 2: deshabilitar SSR para el componente problemático):
import dynamic from 'next/dynamic'
const DynamicTimestamp = dynamic(() => import('./Timestamp'), { ssr: false })
export default function Page() {
return <DynamicTimestamp />
}
2. Verifica metaetiquetas de detección automática en iOS
Agrega esta metaetiqueta en <head> (en app/layout.tsx o app/layout.tsx con next/head):
// app/layout.tsx
import { Metadata } from 'next'
export const metadata: Metadata = {
// ...
other: {
'format-detection': 'telephone=no, date=no, email=no, address=no',
},
}
O directamente en el <head> si usas next/head:
import Head from 'next/head'
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<html>
<head>
<meta name="format-detection" content="telephone=no, date=no, email=no, address=no" />
</head>
<body>{children}</body>
</html>
)
}
3. Usa suppressHydrationWarning solo como último recurso
Solo para casos ineludibles (ej. timestamps, IDs aleatorios):
<time suppressHydrationWarning>{new Date().toISOString()}</time>
⚠️ Importante: Esta solución no corrige el problema, solo silencia la advertencia. React dejará de intentar sincronizar ese nodo, lo que puede causar comportamientos inesperados si se usa abusivamente.
4. Verifica configuraciones de CDN y minificación
Si usas Cloudflare, Vercel Edge Config, o cualquier proxy intermedio:
- Desactiva Auto Minify HTML (Cloudflare).
- Asegúrate de que no se estén inyectando scripts o modificando el HTML prerrendereado.
- Prueba localmente (
next dev) para confirmar si el error desaparece (si sí, el problema está en el entorno de producción).
Bloque de código corregido (ejemplo completo)
// app/page.tsx
import { useState, useEffect } from 'react'
function ClientOnlyTimestamp() {
const [time, setTime] = useState<string>('Cargando...')
useEffect(() => {
setTime(new Date().toLocaleString())
}, [])
return <time suppressHydrationWarning>{time}</time>
}
export default function Page() {
return (
<div>
<h1>Última actualización:</h1>
<ClientOnlyTimestamp />
</div>
)
}
Pro-tip: Diagnóstico rápido
- Abre DevTools > Network > Disable cache.
- Haz Hard Reload (
Ctrl+Shift+R/Cmd+Shift+R). - Busca en la consola: el error muestra el primer nodo divergente (ej.
Expected text "12:00" but received "12:01"). - Usa
console.log(window?.location?.href)solo dentro deuseEffectpara depurar sin afectar SSR.
🔥 Regla de oro: Si algo no es idéntico entre servidor y cliente, no debe estar en el render directo. Usa
useEffect,dynamic({ ssr: false }), osuppressHydrationWarningcon extremo cuidado.
Top comments (0)