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 HTML que React intenta hidratar en el navegador. Es un problema crítico que rompe la hidratación y puede dejar la aplicación inestable o no funcional.

Causa raíz

En tu caso, el error está relacionado con contenido dinámico que cambia entre renderizado en servidor y renderizado en cliente, específicamente con fechas/horas que varían según la zona horaria (como JUN 9, JUN 11, JUN 18 y VIEW EVENTS›). Estas fechas probablemente se generan usando new Date() o APIs dependientes del contexto del navegador (como Intl.DateTimeFormat con zona horaria local), lo cual produce resultados distintos en el servidor (donde no hay window, Date.now() puede ser estático o no estar disponible) y en el cliente.

Además, si usas librerías como date-fns, moment, o manipulación manual de fechas sin considerar SSR, es muy probable que estés inyectando valores distintos en cada entorno.

Solución definitiva (pasos)

✅ Paso 1: Aisla el contenido dinámico no determinista

Identifica dónde se renderizan las fechas dinámicas. Busca en tus componentes:

  • Uso de new Date(), Date.now(), toLocaleDateString(), format(), etc.
  • Lógica que depende de typeof window !== 'undefined' dentro del render.
  • Uso de localStorage, navigator, window.innerWidth, etc.

✅ Paso 2: Usa suppressHydrationWarning solo donde sea estrictamente necesario

Si el contenido es intencionalmente distinto (como un contador de tiempo real, o una fecha "hoy" que cambia), envuelve el elemento problemático con suppressHydrationWarning.

// Ejemplo corregido para una fecha dinámica
<time 
  dateTime={event.date.toISOString()} 
  suppressHydrationWarning
>
  {event.date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
</time>
Enter fullscreen mode Exit fullscreen mode

⚠️ Importante: Esto silencia el error, pero no lo soluciona. Solo úsalo si el contenido debe variar (ej. "Hoy", "Ahora", "Última actualización").

✅ Paso 3: Normaliza fechas en el servidor con zona horaria explícita

Si las fechas deben ser consistentes (ej. eventos programados), no dependas de la zona horaria del cliente ni del servidor. Usa siempre UTC o una zona horaria fija (como UTC o America/New_York):

// ✅ CORRECTO: Usa zonas horarias explícitas y consistentes
import { format } from 'date-fns';
import { utcToZonedTime } from 'date-fns-tz';

const formatDate = (dateStr: string) => {
  const date = new Date(dateStr);
  const timeZone = 'UTC'; // O una zona fija como 'Europe/London'
  const zonedDate = utcToZonedTime(date, timeZone);
  return format(zonedDate, 'MMM d');
};

// En tu componente:
<time dateTime={dateStr} suppressHydrationWarning>
  {formatDate(dateStr)}
</time>
Enter fullscreen mode Exit fullscreen mode

📌 Nota: Si usas date-fns-tz, instálalo: npm install date-fns-tz

✅ Paso 4: Evita lógica condicional en el render (usa useEffect para efectos)

Si necesitas mostrar contenido solo en cliente (ej. "Hoy es jueves"), usa useEffect para actualizar el estado después de la hidratación:

'use client';

import { useState, useEffect } from 'react';

export default function EventDate({ dateStr }: { dateStr: string }) {
  const [formattedDate, setFormattedDate] = useState<string>('');

  useEffect(() => {
    const date = new Date(dateStr);
    setFormattedDate(date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }));
  }, [dateStr]);

  // Renderiza lo mismo en SSR y CSR inicial
  return (
    <time dateTime={dateStr} suppressHydrationWarning>
      {formattedDate || <span aria-hidden="true">Loading...</span>}
    </time>
  );
}
Enter fullscreen mode Exit fullscreen mode

✅ Paso 5: Verifica meta tags en <head>

iOS puede inyectar enlaces automáticamente en fechas y números de teléfono → desactiva detección automática:

// En tu layout.tsx o page.tsx (App Router)
import { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'Events',
  // ...
  other: {
    'meta[name="format-detection"]': 'telephone=no, date=no, email=no, address=no',
  },
};
Enter fullscreen mode Exit fullscreen mode

O directamente en el <head> global (si usas app/layout.tsx):

<head>
  <meta name="format-detection" content="telephone=no, date=no, email=no, address=no" />
</head>
Enter fullscreen mode Exit fullscreen mode

Bloque de código corregido (ejemplo completo)

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

import { useState, useEffect } from 'react';
import { format } from 'date-fns';
import { utcToZonedTime } from 'date-fns-tz';

interface EventDateProps {
  dateStr: string;
  timeZone?: string; // Ej: 'UTC', 'Europe/London'
}

export default function EventDate({ dateStr, timeZone = 'UTC' }: EventDateProps) {
  const [formatted, setFormatted] = useState('');

  useEffect(() => {
    try {
      const date = new Date(dateStr);
      const zoned = utcToZonedTime(date, timeZone);
      setFormatted(format(zoned, 'MMM d'));
    } catch (e) {
      setFormatted('TBD');
    }
  }, [dateStr, timeZone]);

  return (
    <time 
      dateTime={dateStr} 
      suppressHydrationWarning
      className="font-medium"
    >
      {formatted}
    </time>
  );
}
Enter fullscreen mode Exit fullscreen mode

Y en tu layout:

// app/layout.tsx
<head>
  <meta name="format-detection" content="telephone=no, date=no, email=no, address=no" />
</head>
Enter fullscreen mode Exit fullscreen mode

Pro-tip: Diagnóstico rápido

  1. Activa el modo estricto de Next.js en next.config.js:
   module.exports = {
     reactStrictMode: true,
   };
Enter fullscreen mode Exit fullscreen mode
  1. Usa console.error en el render para detectar dónde ocurre el mismatch:
   if (typeof window !== 'undefined') {
     console.warn('Client render detected');
   }
Enter fullscreen mode Exit fullscreen mode
  1. Prueba en modo desarrollo con next dev y revisa la consola del navegador: el error muestra exactamente qué nodo difiere (ej: Expected text content "JUN 9" but received "JUN 10").

  2. Evita librerías de fecha que no sean SSR-safe (ej: moment-timezone sin configuración explícita). Usa date-fns + date-fns-tz o Temporal (en Node 20+).

✅ Con esto, el error desaparecerá definitivamente.

Top comments (0)