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

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

Este error ocurre cuando el HTML generado en el servidor (SSR/SSG) no coincide con el árbol de React generado durante la primera renderización en el navegador (hydration). Es un problema crítico que rompe la experiencia de usuario y puede causar comportamientos impredecibles.


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

En tu caso, el error está directamente relacionado con el uso de APIs del navegador (Date, window, etc.) o contenido dinámico no determinista durante la renderización inicial.

Los elementos más probables en tu código:

  • Uso de new Date() o Date.now() en el render
  • Uso de typeof window !== 'undefined' sin manejo de hydration
  • Meta tags dinámicos (como format-detection) mal insertados
  • Componentes con localStorage, navigator, performance.now(), etc.
  • Extensiones del navegador (poco probable si el error es reproducible en entornos limpios)

⚠️ Nota crítica: El error menciona NEXT.JS NIGHTSSF JUN 9 • AMS JUN 11 • LDN JUN 18. Es muy probable que estés renderizando fechas dinámicas (ej. new Date().toLocaleDateString()) o usando Date.now() para calcular tiempos relativos ("hace 2 horas"), lo cual no es idempotente entre SSR y CSR.


✅ Solución definitiva (pasos verificados)

Paso 1: Identifica el elemento problemático

Busca en tu código:

  • <time> o <span> con fechas calculadas dinámicamente
  • Uso de new Date() en el cuerpo del componente (no solo en useEffect)
  • Lógica condicional basada en window o localStorage en el render

Ejemplo problemático:

// ❌ MAL: Esto se ejecuta en SSR y CSR con valores distintos
export default function EventDate({ date }: { date: string }) {
  const now = new Date(); // ← SSR vs CSR: diferente valor
  const eventDate = new Date(date);
  const diff = eventDate.getTime() - now.getTime();
  const days = Math.ceil(diff / (1000 * 60 * 60 * 24));

  return <span>{days > 0 ? `En ${days} días` : 'Hoy'}</span>;
}
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. "hace X tiempo")

Usa suppressHydrationWarning en el elemento específico + useEffect para actualizarlo:

import { useState, useEffect } from 'react';

export default function RelativeTime({ isoDate }: { isoDate: string }) {
  const [timeAgo, setTimeAgo] = useState<string>('');

  useEffect(() => {
    const update = () => {
      const now = new Date();
      const eventDate = new Date(isoDate);
      const diff = Math.abs(now.getTime() - eventDate.getTime());
      const days = Math.ceil(diff / (1000 * 60 * 60 * 24));
      setTimeAgo(days === 0 ? 'Hoy' : `En ${days} días`);
    };

    update();
    const interval = setInterval(update, 60_000); // Actualizar cada minuto
    return () => clearInterval(interval);
  }, [isoDate]);

  return (
    <time 
      dateTime={isoDate} 
      suppressHydrationWarning
    >
      {timeAgo}
    </time>
  );
}
Enter fullscreen mode Exit fullscreen mode

Ventaja: SSR renderiza el datetime estático (SEO-friendly), hydration no falla, y luego se actualiza dinámicamente.


✅ Caso B: Uso de typeof window !== 'undefined' en render

Nunca uses esto directamente en el render. En su lugar:

// ❌ MAL
const isClient = typeof window !== 'undefined';
return <div>{isClient ? 'Client' : 'Server'}</div>;

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

export default function ClientIndicator() {
  const [isClient, setIsClient] = useState(false);

  useEffect(() => {
    setIsClient(true);
  }, []);

  return <div>{isClient ? 'Client' : 'Server'}</div>;
}
Enter fullscreen mode Exit fullscreen mode

✅ Caso C: Meta tags dinámicos (iOS format-detection)

Si usas format-detection, asegúrate de que esté en <head> y sea estático:

// pages/_document.tsx (o app/layout.tsx con Next.js 13+)
import { Html, Head, Main, NextScript } from 'next/document';

export default function Document() {
  return (
    <Html lang="es">
      <Head>
        {/* ✅ Siempre estático, no condicional */}
        <meta name="format-detection" content="telephone=no, date=no, email=no, address=no" />
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}
Enter fullscreen mode Exit fullscreen mode

Nunca insertes meta tags dinámicamente con next/head dentro de componentes que se rendericen en el cuerpo.


Paso 3: Verificación rápida (debug)

Agrega este useEffect temporalmente en el componente sospechoso para confirmar la causa:

useEffect(() => {
  const el = document.querySelector('[data-test="problematic"]');
  if (el) {
    console.warn('Hydration mismatch detected in:', el);
    console.log('Server HTML:', el.outerHTML);
  }
}, []);
Enter fullscreen mode Exit fullscreen mode

Y en tu JSX:

<div data-test="problematic">
  {/* tu contenido aquí */}
</div>
Enter fullscreen mode Exit fullscreen mode

🛠️ Pro-tip: Prevención a largo plazo

  1. Usa next/dynamic con ssr: false solo como último recurso

    (afecta el rendimiento y SEO)

  2. Valida siempre tus fechas en el servidor

   // utils/formatDate.ts
   export const formatDate = (dateStr: string) => {
     const date = new Date(dateStr);
     if (isNaN(date.getTime())) throw new Error('Invalid date');
     return date.toISOString(); // Siempre devuelve YYYY-MM-DDTHH:mm:ss.sssZ
   };
Enter fullscreen mode Exit fullscreen mode
  1. Herramienta de diagnóstico Ejecuta en modo desarrollo:
   NEXT_PUBLIC_DEBUG_HYDRATION=1 next dev
Enter fullscreen mode Exit fullscreen mode

Y activa el modo React DevToolsHydration (en versiones recientes).

  1. Prueba en modo producción simulado
   npm run build && npm run start
Enter fullscreen mode Exit fullscreen mode

📌 Resumen rápido

Caso Solución Código clave
Fechas dinámicas suppressHydrationWarning + useEffect <time suppressHydrationWarning>
typeof window en render useState(false) + useEffect setIsClient(true)
Meta tags iOS En _document.tsx o layout.tsx <meta content="telephone=no, date=no..." />
APIs del navegador Mover a useEffect o useLayoutEffect useEffect(() => { ... }, [])

Regla de oro: Si algo no es idempotente entre SSR y CSR, no debe estar en el render principal. Usa useEffect para todo lo que dependa del entorno del navegador.

¡Aplica esto y el error desaparecerá para siempre.

Top comments (0)