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 (SSR/SSG) no coincide con el árbol de React generado durante la primera renderización en el cliente (hydration). React detecta una inconsistencia crítica y detiene la hidratación para evitar comportamientos impredecibles.

🔍 Causa raíz (en tu caso específico)

Dado que mencionas "Latest Version 16.2.11" y el contexto de App Router, es muy probable que estés usando Date() o new Date() directamente en el render de un componente, o alguna dependencia (como una librería de UI o un hook personalizado) que lo haga.

Ejemplo típico:

// ❌ Mala práctica: Date() se evalúa en SSR y en CSR, pero con microdiferencias de tiempo
function Timestamp() {
  return <time>{new Date().toISOString()}</time>
}
Enter fullscreen mode Exit fullscreen mode

Otra causa común: iOS Safari modifica el DOM inyectando enlaces automáticamente en lo que detecta como números de teléfono o fechas, lo cual rompe la consistencia entre SSR y CSR.

✅ Solución definitiva (pasos)

Paso 1: Aísla la fuente del desajuste

Busca en tu código (especialmente en app/, layout.tsx, page.tsx, componentes reutilizables) cualquier uso de:

  • new Date(), Date.now(), Math.random(), window, localStorage, navigator, etc.
  • Librerías que usan APIs del navegador sin protección (ej. framer-motion, react-icons sin ssr: false)

Paso 2: Aplica la solución correcta según el caso

🔹 Caso A: Contenido dinámico intencionalmente diferente (ej. timestamps, UUIDs)

Usa suppressHydrationWarning solo en el elemento afectado:

// ✅ Correcto: suprime advertencia *solo* en el elemento problemático
<time 
  datetime={new Date().toISOString()} 
  suppressHydrationWarning
>
  {new Date().toLocaleString()}
</time>
Enter fullscreen mode Exit fullscreen mode

⚠️ Importante: No envuelvas el componente padre en suppressHydrationWarning, solo el elemento con contenido inestable.

🔹 Caso B: Uso de APIs del navegador (ej. window, localStorage)

Mueve la lógica al useEffect o usa dynamic con ssr: false:

// ✅ Opción 1: Con `useEffect`
function ClientOnlyTimestamp() {
  const [date, setDate] = useState<string>('')

  useEffect(() => {
    setDate(new Date().toLocaleString())
  }, [])

  return <time suppressHydrationWarning>{date}</time>
}
Enter fullscreen mode Exit fullscreen mode
// ✅ Opción 2: Con `dynamic` (recomendado para componentes completos)
import dynamic from 'next/dynamic'

const ClientOnlyComponent = dynamic(() => import('./ClientOnlyComponent'), {
  ssr: false,
  loading: () => <span>Cargando...</span>
})

export default function Page() {
  return <ClientOnlyComponent />
}
Enter fullscreen mode Exit fullscreen mode

🔹 Caso C: iOS Safari inyecta enlaces en fechas/teléfonos

Agrega esta etiqueta <meta> en tu 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

Paso 3: Verifica configuraciones de infraestructura

  • Cloudflare Auto Minify: Desactívalo temporalmente para descartar que el minificador de HTML esté alterando el markup.
  • CDNs o proxies intermedios: Algunos inyectan scripts o modifican atributos data-* o class.

🛠 Bloque de código corregido (ejemplo real)

// app/page.tsx
import { useState, useEffect } from 'react'

export default function Page() {
  const [currentTime, setCurrentTime] = useState<string>('')

  useEffect(() => {
    setCurrentTime(new Date().toLocaleTimeString('es-ES', {
      hour: '2-digit',
      minute: '2-digit',
    }))
  }, [])

  return (
    <main className="p-4">
      <h1>Última actualización</h1>
      <p>
        Hora del servidor (estática): 14:30
      </p>
      <p>
        Hora del cliente (dinámica):
        <time suppressHydrationWarning>{currentTime}</time>
      </p>
    </main>
  )
}
Enter fullscreen mode Exit fullscreen mode

💡 Pro-tip: Prevención a largo plazo

  1. Usa linter con reglas de React: Instala eslint-plugin-react-hooks y eslint-plugin-jsx-a11y. Detectan usos peligrosos de Date() o window en render.
  2. Prueba en modo desarrollo: El error aparece en consola con React hydration error, pero no aparece en producción (solo silenciosamente falla). Siempre desarrolla con next dev.
  3. Evita lógica de render condicional basada en typeof window: Usa useEffect o dynamic({ ssr: false }) en su lugar.

Verificación final: Si el error desaparece al deshabilitar JavaScript, el problema es de hidratación. Si persiste, revisa tu HTML generado (View Source vs DevTools Elements).

Top comments (0)