Cómo solucionar "Text content does not match server-rendered HTML" en Next.js App Router
Este error ocurre porque React espera que el HTML generado en el servidor coincida exactamente con el árbol de componentes que genera el navegador en la primera renderización (hydration). Cualquier diferencia —incluso un espacio extra, una etiqueta anidada incorrectamente o una llamada a Date.now()— rompe la coherencia y activa el warning.
Causa raíz (diagnóstico rápido)
En el App Router de Next.js, todos los componentes se renderizan en el servidor por defecto. Si tu código:
- Usa
window,localStorage,Date()o cualquier API del navegador durante la renderización inicial - Genera contenido dinámico no determinista (ej.
Math.random(),new Date()) - Tiene estructuras HTML inválidas (ej.
<p>dentro de<p>) - Usa CSS-in-JS mal configurado (como
styled-componentssin@emotion/reacten SSR) - O hay un middleware/CDN (Cloudflare, Vercel Edge) que modifica el HTML prerenderizado
→ Hydration mismatch inevitable.
Solución definitiva (pasos verificados)
✅ Paso 1: Detecta la fuente exacta del mismatch
- Abre la consola del navegador y busca la línea exacta del error:
Hydration failed because the initial UI does not match what was rendered on the server.
Enter fullscreen mode Exit fullscreen mode
-
Busca el componente afectado: el error suele apuntar al nodo DOM donde ocurre la discrepancia (ej.
<span>,<time>, etc.). - Verifica si hay contenido no determinista:
// ❌ MAL: Date() se evalúa en SSR y en CSR → diferente valor
<span>Última actualización: {new Date().toLocaleString()}</span>
Enter fullscreen mode Exit fullscreen mode
✅ Paso 2: Aplica la solución según el caso
🔹 Caso A: Contenido dinámico no crítico (ej. fecha, hora, random)
Usa suppressHydrationWarning en el elemento específico:
<time
datetime={new Date().toISOString()}
suppressHydrationWarning
>
{new Date().toLocaleDateString()}
</time>
Enter fullscreen mode Exit fullscreen mode
⚠️ Pro-tip: Solo aplica esto al elemento directamente afectado. No en el contenedor padre.
🔹 Caso B: Lógica condicional basada en window o localStorage
Usa useEffect para diferir la renderización client-side:
'use client';
import { useState, useEffect } from 'react';
export default function ClientOnlyComponent() {
const [isClient, setIsClient] = useState(false);
const [user, setUser] = useState(null);
useEffect(() => {
setIsClient(true);
setUser(localStorage.getItem('user') || 'Guest');
}, []);
// SSR: siempre renderiza "Loading..." o valor por defecto
if (!isClient) return <div>Cargando...</div>;
// CSR: usa localStorage sin riesgo
return <div>Hola, {user}!</div>;
}
Enter fullscreen mode Exit fullscreen mode
🔹 Caso C: Componente que solo debe ejecutarse en el cliente
Desactiva SSR con dynamic y ssr: false:
// components/Chart.tsx
'use client';
import dynamic from 'next/dynamic';
const Chart = dynamic(() => import('./Chart'), { ssr: false });
export default function Page() {
return (
<main>
<h1>Estadísticas</h1>
<Chart /> {/* No se renderiza en SSR → evita mismatch */}
</main>
);
}
Enter fullscreen mode Exit fullscreen mode
🔹 Caso D: iOS detecta automáticamente números y los convierte en enlaces
Agrega la meta etiqueta 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>
);
}
Enter fullscreen mode Exit fullscreen mode
🛠️ Verificación final
-
Reinicia el dev server:
npm run dev -
Forza una limpieza de cache:
rm -rf .next && npm run dev - Prueba en modo incógnito (para descartar extensiones como "React DevTools" o "AdBlock")
-
Valida el HTML generado:
curl http://localhost:3000 | grep -A 5 "tu-componente"
Pro-tip: Prevención a largo plazo
-
Nunca uses APIs del navegador fuera de
useEffectouseActionState. -
Evita funciones no puras en render:
Math.random(),Date.now(),fetch()(usafetchen Server Components ouseSWR). -
Valida tu HTML con
w3c-validatorpara detectar anidaciones inválidas. -
En producción, desactiva Auto Minify en Cloudflare (o usa
/* minify off */en el HTML).
✅ Regla de oro: Si el contenido cambia entre SSR y CSR, debe tener
suppressHydrationWarningo retrasarse conuseEffect.
Con esto, el error desaparecerá de forma definitiva.
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.