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 (server-side rendering) no coincide con el árbol de React generado durante la primera renderización del cliente (client-side hydration). React espera que ambos árboles sean idénticos; si hay una discrepancia, lanza este error crítico para evitar inconsistencias de estado y comportamiento impredecible.
🔍 Causa raíz (diagnóstico técnico)
En tu caso, el problema está relacionado con contenido dinámico que varía entre servidor y cliente, como:
- Uso de
Date.now(),new Date(),Math.random()o cualquier valor no determinista en el renderizado. - Acceso directo a
window,localStorage,navigator, etc., durante la renderización (no solo enuseEffect). - Metaetiquetas de detección automática de iOS (
format-detection) que inyectan nodos<a>en el DOM después del SSR, pero antes de la hidratación. - Extensiones del navegador que modifican el DOM (ej. traductores, bloqueadores de anuncios).
- Configuraciones de CDN/Edge (como Cloudflare Auto Minify) que alteran el HTML antes de llegar al cliente.
⚠️ Importante: El error no se debe a errores de sintaxis HTML (como <p> anidado), ya que Next.js App Router valida esto en tiempo de compilación.
✅ Solución definitiva (pasos verificados en producción)
Paso 1: Identifica el elemento problemático
Busca en tu código fuentes de no determinismo:
-
new Date(),Math.random(),Date.now(),performance.now() -
localStorage.getItem('...'),window.innerWidth,navigator.language - Uso de APIs asíncronas sincrónicas (ej.
fetchen el render)
🔍 Ejemplo problemático:
// ❌ Mala práctica: Date() se ejecuta en SSR y en CSR con valores distintos
export default function Timestamp() {
return <time>{new Date().toISOString()}</time>
}
Paso 2: Aplica la solución según el caso
✅ Caso A: Contenido que debe cambiar (ej. fecha actual)
Usa suppressHydrationWarning solo en el elemento específico:
// ✅ Correcto: silencia el warning en el elemento problemático
export default function Timestamp() {
return <time suppressHydrationWarning>{new Date().toISOString()}</time>
}
⚠️ Pro-tip: Nunca uses
suppressHydrationWarningen elementos contenedores (<div>,<section>) ni en componentes complejos. Solo en nodos atómicos (textos,<time>,<span>).
✅ Caso B: Lógica dependiente del cliente (ej. localStorage)
Mueve la lógica a useEffect y renderiza un placeholder en SSR:
// ✅ Correcto: renderiza igual en SSR y CSR inicial
import { useState, useEffect } from 'react'
export default function UserGreeting() {
const [name, setName] = useState<string>('Cargando...')
useEffect(() => {
const storedName = localStorage.getItem('name')
if (storedName) setName(storedName)
}, [])
return <p>Hola, {name}</p>
}
✅ Caso C: iOS inyecta enlaces en números/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: Componentes con lógica compleja (ej. gráficos, mapas)
Desactiva SSR para ese componente:
// components/Chart.tsx
'use client'
import dynamic from 'next/dynamic'
const Chart = dynamic(() => import('./ChartComponent'), { ssr: false })
export default function Page() {
return (
<div>
<h1>Estadísticas</h1>
<Chart /> {/* Se renderiza solo en cliente */}
</div>
)
}
🛠️ Diagnóstico rápido con logs
Agrega este hack temporal para localizar el error:
// En tu componente sospechoso
useEffect(() => {
if (process.env.NODE_ENV === 'development') {
console.log('Hydration mismatch check:', {
serverHTML: document.documentElement.innerHTML,
clientHTML: document.documentElement.innerHTML,
})
}
}, [])
🔥 Pro-tip: Usa
next dev --turbopara activar el modo estricto de hidratación y detectar desajustes más rápido.
🚫 Errores comunes que debes evitar
| Error | Solución |
|---|---|
typeof window !== 'undefined' en el render |
Mover a useState + useEffect
|
localStorage en el cuerpo del componente |
Usar useState inicializado con valor por defecto |
Math.random() en JSX |
Generar valor en useEffect o usar suppressHydrationWarning
|
<a> dentro de <a> (aunque sea invisible) |
Validar con next lint y corregir anidación |
✅ Verificación final
- Ejecuta
next build→ el build debe fallar si hay errores de hidratación. - En desarrollo, el error aparece en consola antes de que la app sea interactiva.
- Usa
React DevTools→ marca roja en componentes consuppressHydrationWarning.
Si el error persiste, revisa configuraciones de CDN (Cloudflare, Vercel Edge Config) y desactiva Auto Minify temporalmente.
💡 Último consejo: Si usas librerías de terceros (ej.
date-fns,dayjs), asegúrate de que no usen APIs del navegador en su construcción (verificapackage.json→"sideEffects": falsepuede causar problemas si no se configura correctamente).
Top comments (0)