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()oDate.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 usandoDate.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 enuseEffect) - Lógica condicional basada en
windowolocalStorageen 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>;
}
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>
);
}
✅ Ventaja: SSR renderiza el
datetimeestá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>;
}
✅ 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>
);
}
❗ Nunca insertes meta tags dinámicamente con
next/headdentro 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);
}
}, []);
Y en tu JSX:
<div data-test="problematic">
{/* tu contenido aquí */}
</div>
🛠️ Pro-tip: Prevención a largo plazo
Usa
next/dynamicconssr: falsesolo como último recurso
(afecta el rendimiento y SEO)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
};
- Herramienta de diagnóstico Ejecuta en modo desarrollo:
NEXT_PUBLIC_DEBUG_HYDRATION=1 next dev
Y activa el modo React DevTools → Hydration (en versiones recientes).
- Prueba en modo producción simulado
npm run build && npm run start
📌 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
useEffectpara todo lo que dependa del entorno del navegador.
¡Aplica esto y el error desaparecerá para siempre.
Top comments (0)