DEV Community

Erick Eduardo Ramos
Erick Eduardo Ramos

Posted on

Cómo solucionar el error \"Text content does not match server-rendered HTML\" en Next.js App Router

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 en useEffect).
  • 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. fetch en 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>
}
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

⚠️ Pro-tip: Nunca uses suppressHydrationWarning en 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>
}
Enter fullscreen mode Exit fullscreen mode

✅ 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>
  )
}
Enter fullscreen mode Exit fullscreen mode

✅ 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>
  )
}
Enter fullscreen mode Exit fullscreen mode

🛠️ 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,
    })
  }
}, [])
Enter fullscreen mode Exit fullscreen mode

🔥 Pro-tip: Usa next dev --turbo para 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

  1. Ejecuta next build → el build debe fallar si hay errores de hidratación.
  2. En desarrollo, el error aparece en consola antes de que la app sea interactiva.
  3. Usa React DevTools → marca roja en componentes con suppressHydrationWarning.

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 (verifica package.json"sideEffects": false puede causar problemas si no se configura correctamente).

Top comments (0)