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 (renderToString) no coincide con el árbol de React generado durante la primera renderización en el cliente (hydration). Es un fallo crítico de consistencia que rompe la experiencia de usuario y puede afectar el SEO.

Causa raíz

En tu caso, el problema está directamente relacionado con el uso de APIs sensibles al tiempo o contexto del cliente en el renderizado inicial, como Date(), new Date(), Date.now(), o incluso funciones que dependen de Date.now() para generar contenido dinámico (por ejemplo, fechas relativas como "hace 5 minutos").

El servidor genera un valor estático (ej. Mon Jun 09 2025 10:00:00 GMT+0000), pero al hidratarse en el cliente, el navegador genera un valor distinto (aunque sea milisegundos después), causando el mismatch.

Otras causas comunes en tu contexto:

  • Uso de typeof window !== 'undefined' directamente en el render.
  • Uso de localStorage, sessionStorage, navigator, etc., sin protección.
  • Meta tags dinámicos que incluyen fechas/horas actuales.
  • Extensiones del navegador (como Dark Reader) que modifican el DOM antes de la hidratación.
  • Servidores proxy/CDN que minifican HTML (Cloudflare Auto Minify es un culpable frecuente).

Solución definitiva (pasos verificados en producción)

✅ Paso 1: Identifica la fuente exacta del mismatch

Busca en tu código:

  • Cualquier uso directo de Date(), new Date(), Date.now(), toLocaleString(), toISOString() en componentes que se renderizan en el <body>.
  • Fechas relativas calculadas en el render (ej. new Date() - createdDate).
  • Componentes que renderizan Date.now() o performance.now().

🔍 Comando útil para detectar sospechosos:

grep -r "new Date()" src/app
grep -r "Date.now()" src/app
grep -r "toLocaleString()" src/app
Enter fullscreen mode Exit fullscreen mode

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

🟢 Caso A: Contenido que debe cambiar en el cliente (ej. timestamp actual)

Usa suppressHydrationWarning en el elemento específico:

// ✅ CORRECTO: Solo suprime el warning en el elemento problemático
<time 
  dateTime={new Date().toISOString()} 
  suppressHydrationWarning
>
  {new Date().toLocaleString()}
</time>
Enter fullscreen mode Exit fullscreen mode

⚠️ Importante: No lo uses en elementos contenedores (como <div>), solo en el nodo de texto exacto que varía.

🟢 Caso B: Lógica condicional basada en window/localStorage

Envuelve la lógica con useEffect + estado local:

'use client'

import { useState, useEffect } from 'react'

export default function ClientOnlyComponent() {
  const [isClient, setIsClient] = useState(false)
  const [user, setUser] = useState<string | null>(null)

  useEffect(() => {
    setIsClient(true)
    setUser(localStorage.getItem('user')) // ✅ Seguro: solo se ejecuta en cliente
  }, [])

  if (!isClient) return <span>Cargando...</span> // ✅ Mismo HTML en SSR y primer client render

  return <span>Hola, {user || 'invitado'}!</span>
}
Enter fullscreen mode Exit fullscreen mode

🟢 Caso C: Componente que nunca debe SSR (ej. reloj en tiempo real, canvas)

Desactiva SSR con next/dynamic:

// components/RealTimeClock.tsx
'use client'
export default function RealTimeClock() {
  const [time, setTime] = useState(new Date())

  useEffect(() => {
    const timer = setInterval(() => setTime(new Date()), 1000)
    return () => clearInterval(timer)
  }, [])

  return <time>{time.toLocaleTimeString()}</time>
}

// page.tsx
import dynamic from 'next/dynamic'

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

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

✅ Paso 3: Verifica configuración de CDNs/Proxies

Si usas Cloudflare, Vercel Edge Config, o cualquier proxy que modifique HTML:

  • Cloudflare: Desactiva Auto Minify (HTML) en Speed > Optimization.
  • Vercel: Asegúrate de no tener middleware que modifique response.text() sin preservar el DOM.

Pro-Tip: Diagnóstico rápido en desarrollo

  1. Reproduce el error en modo estricto:
   NEXT_STRICT_MODE=1 next dev
Enter fullscreen mode Exit fullscreen mode

Esto activa doble renderizado en desarrollo y muestra el mismatch exacto en consola.

  1. Inspecciona el HTML pre-hidratación:

    • Abre DevTools > Network > marca Disable cache.
    • Recarga la página con Ctrl+Shift+R (o Cmd+Shift+R).
    • Busca en el panel Elements el texto que difiere entre "Server" y "Client" (verás una marca amarilla en el DOM).
  2. Herramienta de validación:

   npx next telemetry disable
   npx next build && npx next export
Enter fullscreen mode Exit fullscreen mode

Si el error desaparece en export, el problema está en SSR dinámico (no en hidratación real).


Bonus: Solución para iOS (caso frecuente en apps móviles)

Si usas fechas o números que iOS convierte en enlaces automáticamente:

Agrega en app/layout.tsx:

<head>
  <meta 
    name="format-detection" 
    content="telephone=no, date=no, email=no, address=no" 
  />
</head>
Enter fullscreen mode Exit fullscreen mode

Esto evita que Safari inyecte <a href="tel:..."> o <a href="mailto:..."> después del SSR, causando mismatches silenciosos.


Resultado esperado: El error desaparece sin suprimir warnings innecesarios, y tu app mantiene SSR seguro y optimizado.

Top comments (0)