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

Causa raíz

La causa más frecuente en aplicaciones modernas es el uso de APIs del navegador (window, localStorage, Date.now(), etc.) o lógica condicional basada en typeof window !== 'undefined' dentro del renderizado inicial. Esto provoca que el servidor renderice una versión, y el cliente otra.

En tu caso, el error está relacionado con contenido dinámico (probablemente una versión, timestamp o detección de dispositivo) que cambia entre SSR y CSR.

Solución definitiva (3 pasos)

Paso 1: Aisla el código no SSR-safe

Busca en tu layout o componentes globales (especialmente en app/layout.tsx, app/page.tsx o componentes de menú) cualquier uso de:

  • new Date()
  • localStorage.getItem()
  • window.innerWidth
  • navigator.userAgent
  • Math.random()
  • typeof window !== 'undefined' en el cuerpo del componente

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

✅ Caso A: Contenido que debe ser dinámico (ej. versión, fecha actual)

// app/layout.tsx o componente global
import { useEffect, useState } from 'react';

function ClientOnlyVersion() {
  const [version, setVersion] = useState<string>('...');

  useEffect(() => {
    // Solo se ejecuta en el cliente
    const fetchVersion = async () => {
      // Ejemplo: leer de localStorage o API
      const stored = localStorage.getItem('app-version');
      setVersion(stored || '16.3.8');
    };
    fetchVersion();
  }, []);

  return <span className="text-sm opacity-70">Latest Version {version}</span>;
}

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}
        <ClientOnlyVersion />
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

✅ Caso B: Contenido que no puede diferir (ej. texto estático)

Asegúrate de que todo el contenido visible en el primer render sea idéntico en SSR y CSR. Elimina cualquier lógica condicional en el render.

✅ Caso C: Uso de suppressHydrationWarning (último recurso)

// Solo si el contenido es *intencionalmente* diferente (ej. timestamp)
<footer>
  <time dateTime={new Date().toISOString()} suppressHydrationWarning>
    {new Date().toLocaleDateString()}
  </time>
</footer>
Enter fullscreen mode Exit fullscreen mode

Paso 3: Verifica configuraciones externas

  • Cloudflare / CDN: Desactiva "Auto Minify" o "Rocket Loader" temporalmente para descartar modificación del HTML.
  • Extensiones del navegador: Prueba en modo incógnito.
  • iOS Format Detection: Ya incluido en el <meta> anterior.

Bloque de código corregido (ejemplo práctico)

// app/layout.tsx
import './globals.css';
import { Inter } from 'next/font/google';

const inter = Inter({ subsets: ['latin'] });

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 className={inter.className}>
        <header className="flex items-center justify-between px-4 py-3">
          <h1 className="text-xl font-bold">MenuUsing App Router</h1>
          {/* Solución: Componente client-only para contenido dinámico */}
          <ClientOnlyVersion />
        </header>
        <main>{children}</main>
      </body>
    </html>
  );
}

// components/ClientOnlyVersion.tsx
'use client';

import { useEffect, useState } from 'react';

export default function ClientOnlyVersion() {
  const [version, setVersion] = useState<string>('16.3.8');

  useEffect(() => {
    // Simulación: obtención dinámica de versión
    const getVersion = async () => {
      try {
        const res = await fetch('/api/version');
        if (res.ok) {
          const data = await res.json();
          setVersion(data.version);
        }
      } catch {
        // Fallback seguro
        setVersion('16.3.8');
      }
    };
    getVersion();
  }, []);

  return (
    <span className="text-sm opacity-70">
      Latest Version {version}
    </span>
  );
}
Enter fullscreen mode Exit fullscreen mode

Pro-tip: Prevención definitiva

  1. Regla de oro: Todo lo que se renderice en el <head> o en componentes globales (layout, navbar, footer) debe ser idéntico en SSR y CSR.
  2. Usa next/dynamic con ssr: false solo cuando sea estrictamente necesario.
  3. En pruebas locales, activa el modo estricto de React:
   // app/layout.tsx
   'use client';
   import { StrictMode } from 'react';
   // ...
   <StrictMode>{children}</StrictMode>
Enter fullscreen mode Exit fullscreen mode
  1. Usa la extensión React DevTools para inspeccionar el árbol de hidratación.

⚠️ Nunca uses suppressHydrationWarning como solución principal. Es un escape hatch para casos específicos (fechas, IDs aleatorios), no una solución general.

Top comments (0)