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 lo que React intenta renderizar en el navegador durante la hidratación. Es un problema crítico que rompe la experiencia de usuario y puede afectar el SEO.

¿Por qué ocurre?

En Next.js App Router, el servidor genera HTML estático que luego se "hidrata" en el cliente. Si el árbol de React en el navegador difiere en estructura o contenido del HTML inicial, React lanza este error. Las causas más comunes:

  • Uso de APIs del navegador (window, localStorage, Date.now()) en el renderizado inicial.
  • Lógica condicional basada en typeof window !== 'undefined' dentro del render.
  • Componentes con estado inicial diferente entre servidor y cliente (ej. useState con valor no determinista).
  • Metaetiquetas de detección automática en iOS (format-detection) que inyectan enlaces <a> dinámicamente.
  • Minificación automática de CDN (Cloudflare, Vercel Edge) que altera el HTML prerrendereado.

Solución definitiva (pasos verificados)

✅ Paso 1: Identifica la fuente del desajuste

Busca en tu código:

  • Date(), new Date(), Math.random(), window, localStorage, navigator, etc.
  • Uso de useState con valores no deterministas (ej. useState(() => Math.random()))
  • Componentes que renderizan contenido diferente según el entorno sin protección

🔍 Herramienta útil: Usa console.log('client') dentro de componentes para ver si se ejecutan en SSR (no aparecerá en logs del servidor).


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

Caso A: Contenido dinámico (fechas, IDs, tokens)

Solución recomendada: suppressHydrationWarning en el elemento específico.

// ❌ Mal: desajuste directo
<p>Última actualización: {new Date().toLocaleString()}</p>

// ✅ Bien: suprime advertencia en el elemento problemático
<p suppressHydrationWarning>
  Última actualización: {new Date().toLocaleString()}
</p>
Enter fullscreen mode Exit fullscreen mode

⚠️ Importante: Solo usa suppressHydrationWarning en elementos textuales y de bajo impacto visual. Nunca en contenedores estructurales.


Caso B: Lógica condicional basada en entorno

Solución recomendada: Usa useEffect para diferir la lógica client-side.

'use client'

import { useState, useEffect } from 'react'

export default function ClientOnlyComponent() {
  const [data, setData] = useState<string>('Cargando...')

  useEffect(() => {
    // Solo se ejecuta en el cliente
    const stored = localStorage.getItem('user')
    setData(stored || 'Invitado')
  }, [])

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

Ventaja: El servidor renderiza "Cargando...", y el cliente lo actualiza sin desajuste.


Caso C: Componente totalmente client-side (ej. gráficos, canvas)

Solución recomendada: Deshabilita SSR con next/dynamic.

// components/Chart.tsx
'use client'
import dynamic from 'next/dynamic'

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

export default function Page() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Chart />
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Ventaja: El componente no se renderiza en el servidor → cero riesgo de desajuste.


✅ Paso 3: Solución global para iOS (frecuente en App Router)

Agrega esta metaetiqueta en <head> de tu layout principal (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

📱 Por qué: iOS inyecta automáticamente <a href="tel:..."> en números de teléfono, rompiendo el HTML prerrendereado.


Pro-tip: Prevención definitiva

  1. Nunca uses APIs del navegador en el render top-level:
   // ❌ Evitar
   const time = new Date().toISOString()
   return <time>{time}</time>
Enter fullscreen mode Exit fullscreen mode
  1. Valida siempre estructuras HTML:

    • <p> no puede contener <div>, <ul>, <button>, etc.
    • <a> no puede contener otro <a> ni <button>.
  2. En entornos de producción:

    • Desactiva minificación automática de HTML en Cloudflare (o configura reglas para excluir /app/**).
    • Usa next.config.js para deshabilitar optimizaciones problemáticas:
     module.exports = {
       // Solo si es estrictamente necesario
       // experimental: { optimizePackageImports: true },
     }
    
  3. Prueba en modo estricto:

   npm run dev -- --turbo
   # o
   npm run build && npm run start
Enter fullscreen mode Exit fullscreen mode

¿Sigues teniendo el error?

  • Verifica extensiones del navegador: Desactiva todas y prueba en modo incógnito.
  • Revisa tu CDN/Edge: Cloudflare, Fastly, o Vercel Edge Functions pueden alterar el HTML.
  • Usa console.log en el servidor: Añade console.log('Server render:', JSON.stringify(data)) para comparar con el cliente.

🔥 Último recurso: Usa suppressHydrationWarning en el <html> o <body> como medida de último momento — solo si entiendes los riesgos (pierdes validación de hidratación en toda la página).

Con esto, el error desaparecerá en <5 minutos. ¡A codear!

Top comments (0)