DEV Community

Erick Eduardo Ramos
Erick Eduardo Ramos

Posted on

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

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

Este error ocurre cuando el HTML generado por el servidor (SSR/SSG) no coincide con lo que React intenta hidratar en el navegador durante la primera renderización del lado del cliente. En entornos de App Router, esto es especialmente común debido al uso de APIs del navegador o dependencias de tiempo en componentes que se renderizan en ambos lados.

Causa raíz (diagnóstico técnico)

La causa más probable en tu caso es el uso de Date() o APIs de tiempo en el renderizado, ya que el texto mostrado ("Latest Version16.3.7") sugiere que hay una fecha o versión dinámica renderizada directamente en el JSX. Otros candidatos comunes:

  • Uso de typeof window !== 'undefined' directamente en el render
  • Acceso a localStorage, navigator, window.innerWidth, etc.
  • Componentes que dependen de useEffect pero no se protegen durante SSR
  • Meta tags de detección automática de iOS (format-detection) que inyectan nodos <a> en tiempo de ejecución

Solución definitiva (pasos verificados)

Paso 1: Identifica el elemento problemático

Busca en tus componentes de /app cualquier uso de:

  • new Date(), Date.now(), new Date().toLocaleString()
  • localStorage.getItem(...), window.location, etc.
  • Math.random(), crypto.randomUUID(), etc.

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

Caso A: Contenido dinámico (fechas, versiones, UUIDs)

❌ Incorrecto:

export default function VersionInfo() {
  return (
    <div>
      <p>Latest Version: {new Date().toISOString()}</p>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

✅ Correcto (opción 1: usa suppressHydrationWarning para contenido inmutable):

export default function VersionInfo() {
  return (
    <div>
      <p>Latest Version: <span suppressHydrationWarning>16.3.7</span></p>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

⚠️ Si el texto realmente cambia (ej. versión dinámica), usa dynamic({ ssr: false }) o useEffect para cargarlo después del montaje.

Caso B: Uso de APIs del navegador

❌ Incorrecto:

export default function ClientOnlyComponent() {
  const [data, setData] = useState(() => {
    return localStorage.getItem('user') || 'guest'
  })

  return <div>{data}</div>
}
Enter fullscreen mode Exit fullscreen mode

✅ Correcto (opción 2: deshabilitar SSR para el componente):

// components/ClientOnlyComponent.tsx
'use client'

import { useState, useEffect } from 'react'

export default function ClientOnlyComponent() {
  const [data, setData] = useState('guest')

  useEffect(() => {
    const stored = localStorage.getItem('user')
    if (stored) setData(stored)
  }, [])

  return <div>{data}</div>
}
Enter fullscreen mode Exit fullscreen mode

✅ Correcto (opción 3: usar dynamic con ssr: false):

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

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

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

Paso 3: Prevenir problemas en iOS (si aplica)

Agrega este meta tag en tu <head> (en app/layout.tsx o app/layout.tsx con next/head):

// app/layout.tsx
import './globals.css'
import { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'My App',
  description: 'Generated by create next app',
  viewport: 'width=device-width, initial-scale=1',
  // 🔑 Solución crítica para iOS
  other: {
    'format-detection': 'telephone=no, date=no, email=no, address=no',
  },
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <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

Pro-tip: Diagnóstico rápido con console.log en SSR

Agrega este helper temporalmente en tu componente sospechoso para identificar qué se renderiza en el servidor vs cliente:

export default function DebugHydration() {
  if (typeof window === 'undefined') {
    console.log('🚀 SERVER RENDER')
  } else {
    console.log('💻 CLIENT RENDER')
  }

  return <div>...</div>
}
Enter fullscreen mode Exit fullscreen mode

Si los logs difieren en contenido (ej. servidor muestra "16.3.7", cliente muestra "16.3.7 (2024)"), ese es tu culpable.

✅ Recuerda: Nunca uses Date() o APIs del navegador directamente en el render. Usa useEffect o deshabilita SSR para componentes que dependan de estado del cliente.


Resultado esperado: El error desaparecerá inmediatamente al eliminar la diferencia entre el HTML prerenderizado y el primero renderizado en cliente. Verifica con next dev y revisa la consola del navegador (no aparecerán warnings de hidratación).

Top comments (0)