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 (SSR/SSG) no coincide con lo que React genera en el primer render del cliente durante la hidratación. Es un problema crítico que rompe la experiencia de usuario y puede afectar el rendimiento y SEO.
Causa raíz
La raíz del problema es inconsistencia en el árbol de React entre SSR y hidratación. En entornos de App Router, esto suele deberse a:
- Uso de APIs del navegador (
window,localStorage,Date.now()) durante el render - Lógica condicional basada en
typeof window !== 'undefined'en el cuerpo del componente - Componentes que dependen de estado inicial no determinista (ej. fechas, aleatoriedad)
- Metaetiquetas de detección automática en iOS (
format-detection) - Minificación automática por CDN (Cloudflare Auto Minify)
Solución definitiva (pasos verificados)
✅ Paso 1: Identificar el elemento problemático
Busca en el stack trace del error el componente y la línea exacta donde ocurre la discrepancia. El mensaje suele indicar algo como:
"Text content '2024-05-10' does not match server-rendered HTML '2024-05-11'"
✅ Paso 2: Aplicar la solución según el caso
Caso A: Contenido dinámico no crítico (fechas, IDs, etc.)
Usa suppressHydrationWarning solo en el elemento específico:
// ✅ CORRECTO
<time
dateTime={new Date().toISOString()}
suppressHydrationWarning
>
{new Date().toLocaleDateString()}
</time>
⚠️ Importante: suppressHydrationWarning solo funciona en el elemento directo donde ocurre la diferencia. No se hereda a hijos.
Caso B: Lógica condicional basada en window
Mueve la lógica client-only a useEffect:
// ❌ INCORRECTO
function Clock() {
const [time, setTime] = useState(new Date().toLocaleTimeString());
return <span>{time}</span>;
}
// ✅ CORRECTO
import { useState, useEffect } from 'react';
function Clock() {
const [time, setTime] = useState('');
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
const updateTime = () => setTime(new Date().toLocaleTimeString());
updateTime();
const interval = setInterval(updateTime, 1000);
return () => clearInterval(interval);
}, []);
return <span>{isClient ? time : 'Cargando...'}</span>;
}
Caso C: Componente totalmente client-side
Desactiva SSR con next/dynamic:
// components/ClientOnlyComponent.tsx
export default function ClientOnlyComponent() {
// Lógica que usa window, localStorage, etc.
return <div>Contenido client-only</div>;
}
// app/page.tsx
import dynamic from 'next/dynamic';
const ClientOnlyComponent = dynamic(
() => import('../components/ClientOnlyComponent'),
{ ssr: false }
);
export default function Page() {
return <ClientOnlyComponent />;
}
✅ Paso 3: Prevenir errores iOS
Agrega esta metaetiqueta en <head> de layout.tsx:
// app/layout.tsx
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}</body>
</html>
);
}
✅ Paso 4: Verificar configuración de CDN
Si usas Cloudflare:
- Desactiva Auto Minify (HTML)
- Verifica que no haya reglas que modifiquen el HTML de respuesta
Pro-tip: Diagnóstico rápido
- Abre DevTools → Network → marca "Disable cache"
- Recarga con
Ctrl+Shift+R(oCmd+Shift+R) - Busca en la consola:
Hydration failed because the server rendered HTML didn't match the DOM - Haz clic en el enlace del stack trace → verás el componente exacto y la diferencia
Regla de oro: Si el contenido cambia entre SSR y primer render, debe estar protegido con
suppressHydrationWarning,useEffect, ossr: false. No hay otra solución segura.
Con esto, el error desaparecerá definitivamente.
Top comments (0)