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>
⚠️ 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>
📌 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>
);
}
✅ 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',
},
};
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>
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>
);
}
Y en tu layout:
// app/layout.tsx
<head>
<meta name="format-detection" content="telephone=no, date=no, email=no, address=no" />
</head>
Pro-tip: Diagnóstico rápido
-
Activa el modo estricto de Next.js en
next.config.js:
module.exports = {
reactStrictMode: true,
};
-
Usa
console.erroren el render para detectar dónde ocurre el mismatch:
if (typeof window !== 'undefined') {
console.warn('Client render detected');
}
Prueba en modo desarrollo con
next devy revisa la consola del navegador: el error muestra exactamente qué nodo difiere (ej:Expected text content "JUN 9" but received "JUN 10").Evita librerías de fecha que no sean SSR-safe (ej:
moment-timezonesin configuración explícita). Usadate-fns+date-fns-tzoTemporal(en Node 20+).
✅ Con esto, el error desaparecerá definitivamente.
Top comments (0)