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 en el servidor (SSR/SSG) no coincide con lo que React genera en el primer render del cliente durante la hidratación. Es un problema crítico que rompe la experiencia de usuario y puede afectar el rendimiento y SEO.

Causa raíz

La raíz del problema es inconsistencia en el árbol de React entre SSR y hidratación. En entornos de App Router, esto suele deberse a:

  • Uso de APIs del navegador (window, localStorage, Date.now()) durante el render
  • Lógica condicional basada en typeof window !== 'undefined' en el cuerpo del componente
  • Componentes que dependen de estado inicial no determinista (ej. fechas, aleatoriedad)
  • Metaetiquetas de detección automática en iOS (format-detection)
  • Minificación automática por CDN (Cloudflare Auto Minify)

Solución definitiva (pasos verificados)

✅ Paso 1: Identificar el elemento problemático

Busca en el stack trace del error el componente y la línea exacta donde ocurre la discrepancia. El mensaje suele indicar algo como:

"Text content '2024-05-10' does not match server-rendered HTML '2024-05-11'"

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

Caso A: Contenido dinámico no crítico (fechas, IDs, etc.)

Usa suppressHydrationWarning solo en el elemento específico:

// ✅ CORRECTO
<time 
  dateTime={new Date().toISOString()} 
  suppressHydrationWarning
>
  {new Date().toLocaleDateString()}
</time>
Enter fullscreen mode Exit fullscreen mode

⚠️ Importante: suppressHydrationWarning solo funciona en el elemento directo donde ocurre la diferencia. No se hereda a hijos.

Caso B: Lógica condicional basada en window

Mueve la lógica client-only a useEffect:

// ❌ INCORRECTO
function Clock() {
  const [time, setTime] = useState(new Date().toLocaleTimeString());
  return <span>{time}</span>;
}

// ✅ CORRECTO
import { useState, useEffect } from 'react';

function Clock() {
  const [time, setTime] = useState('');
  const [isClient, setIsClient] = useState(false);

  useEffect(() => {
    setIsClient(true);
    const updateTime = () => setTime(new Date().toLocaleTimeString());
    updateTime();
    const interval = setInterval(updateTime, 1000);
    return () => clearInterval(interval);
  }, []);

  return <span>{isClient ? time : 'Cargando...'}</span>;
}
Enter fullscreen mode Exit fullscreen mode

Caso C: Componente totalmente client-side

Desactiva SSR con next/dynamic:

// components/ClientOnlyComponent.tsx
export default function ClientOnlyComponent() {
  // Lógica que usa window, localStorage, etc.
  return <div>Contenido client-only</div>;
}

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

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

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

✅ Paso 3: Prevenir errores iOS

Agrega esta metaetiqueta en <head> de 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 4: Verificar configuración de CDN

Si usas Cloudflare:

  • Desactiva Auto Minify (HTML)
  • Verifica que no haya reglas que modifiquen el HTML de respuesta

Pro-tip: Diagnóstico rápido

  1. Abre DevTools → Network → marca "Disable cache"
  2. Recarga con Ctrl+Shift+R (o Cmd+Shift+R)
  3. Busca en la consola: Hydration failed because the server rendered HTML didn't match the DOM
  4. Haz clic en el enlace del stack trace → verás el componente exacto y la diferencia

Regla de oro: Si el contenido cambia entre SSR y primer render, debe estar protegido con suppressHydrationWarning, useEffect, o ssr: false. No hay otra solución segura.

Con esto, el error desaparecerá definitivamente.

Top comments (0)