Cómo solucionar el error "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 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
La discrepancia suele provenir de:
- Uso de APIs del navegador (
window,localStorage,Date.now()) durante el renderizado. - Lógica condicional que depende del entorno (
typeof window !== 'undefined') en el body del componente (no dentro deuseEffect). - Extensiones del navegador que inyectan elementos (como ad-blockers o traductores).
- Metaetiquetas de detección automática en iOS (
format-detection) que modifican el DOM. - Minificación automática por CDN (Cloudflare Auto Minify, Vercel Edge Config mal configurado).
Solución definitiva (pasos verificados)
✅ Paso 1: Localiza la fuente exacta del mismatch
- Abre la consola del navegador.
- Haz clic en el error → ve a la pestaña "Sources" → busca el archivo donde ocurre.
- Busca líneas como:
<div>Prerendered: {someClientOnlyValue}</div>
o
<span>{new Date().toLocaleString()}</span>
✅ Paso 2: Aplica la solución según el caso
🔹 Caso A: Contenido dinámico (fechas, IDs aleatorios, etc.)
Usa suppressHydrationWarning en el elemento específico:
<time suppressHydrationWarning>{new Date().toISOString()}</time>
⚠️ No lo uses en contenedores grandes (ej. <div suppressHydrationWarning>...</div>), solo en nodos atómicos.
🔹 Caso B: Lógica condicional basada en window
Mueve la lógica client-side a useEffect + estado local:
'use client';
import { useState, useEffect } from 'react';
export default function ClientComponent() {
const [isClient, setIsClient] = useState(false);
const [time, setTime] = useState('');
useEffect(() => {
setIsClient(true);
setTime(new Date().toLocaleTimeString());
}, []);
return (
<div>
{/* Server-safe render */}
{!isClient && <span>Cargando...</span>}
{/* Client-only render */}
{isClient && <span>Hora actual: {time}</span>}
</div>
);
}
🔹 Caso C: Componentes que usan localStorage/window
Desactiva SSR para ese componente:
// components/ClientOnly.tsx
'use client';
import dynamic from 'next/dynamic';
const ClientOnly = dynamic(() => import('./ClientOnlyComponent'), {
ssr: false,
});
export default function Page() {
return (
<div>
<ClientOnly />
</div>
);
}
🔹 Caso D: iOS detecta automáticamente enlaces en fechas/teléfonos
Agrega esta metaetiqueta en app/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>
);
}
Pro-tip: Prevención proactiva
-
Nunca uses APIs del navegador fuera de
useEffectouseLayoutEffect. -
Evita
Math.random()oDate()en el renderizado directo. -
Usa
next/dynamicconssr: falsepara librerías client-only (ej.react-chartjs-2,framer-motion). -
Desactiva minificación automática en Cloudflare (si usas
Auto Minify, desactívalo para/app). -
Valida con
next build && next start(no connext dev, que omite SSR).
💡 Debug rápido: Ejecuta
next buildlocalmente y revisa los logs de "Hydration failed". Te dirá exactamente qué línea y componente falla.
Bloque de código corregido (ejemplo real)
❌ Antes (error):
export default function Timestamp() {
return <p>Hoy es {new Date().toLocaleDateString()}</p>;
}
✅ Después (correcto):
'use client';
import { useState, useEffect } from 'react';
export default function Timestamp() {
const [date, setDate] = useState('');
useEffect(() => {
setDate(new Date().toLocaleDateString());
}, []);
return <p suppressHydrationWarning>Hoy es {date}</p>;
}
Con esto, el error desaparecerá definitivamente.
Top comments (0)