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 intenta hidratar en el navegador. Es un problema de coherencia entre el DOM estático y el árbol virtual inicial, no un error de lógica funcional.
Causa raíz técnica
La mayoría de los casos provienen de código no determinista en tiempo de renderizado, como:
- Uso directo de
window,localStorage,Date.now()onew Date()en el render - Lógica condicional basada en
typeof window !== 'undefined'dentro del render - APIs del navegador que no existen en Node.js
- Extensiones del navegador (especialmente en desarrollo) que inyectan elementos
- Metaetiquetas de iOS que transforman automáticamente contenido (ej. números telefónicos → enlaces)
Solución definitiva (pasos verificados)
✅ Paso 1: Identifica el elemento problemático
Busca en el stack trace o en la consola del navegador el mensaje exacto:
Warning: Text content "..." does not match server-rendered "..."
Localiza el componente y el elemento HTML afectado.
✅ 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:
// ❌ Mal: fecha generada en render
<span>{new Date().toLocaleDateString()}</span>
// ✅ Bien: con supresión explícita
<time suppressHydrationWarning>{new Date().toLocaleDateString()}</time>
Caso B: Uso de APIs del navegador (localStorage, window.innerWidth, etc.)
Mueve la lógica a useEffect o usa dynamic({ ssr: false }):
// ❌ Mal: acceso directo a window
const [width, setWidth] = useState(window.innerWidth);
// ✅ Bien: con useEffect + estado inicial seguro
import { useState, useEffect } from 'react';
export default function Responsive() {
const [width, setWidth] = useState(0); // Inicializa con valor neutral
useEffect(() => {
setWidth(window.innerWidth);
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return <div>Width: {width}</div>;
}
Caso C: Componente 100% client-side (ej. gráficos, canvas, mapas)
Desactiva SSR con next/dynamic:
// components/MapChart.tsx
export default function MapChart() {
// Código que usa window, document, etc.
return <div ref={mapRef} />;
}
// page.tsx
import dynamic from 'next/dynamic';
const MapChart = dynamic(() => import('../components/MapChart'), {
ssr: false,
loading: () => <p>Cargando mapa...</p>
});
export default function Page() {
return <MapChart />;
}
✅ Paso 3: Verifica configuraciones externas
- Desactiva extensiones del navegador (especialmente ad-blockers, traductores, lectores de pantalla)
- Revisa configuraciones de CDN/Edge (Cloudflare Auto Minify, Fastly Edge Side Includes)
- Agrega metaetiqueta para iOS si usas fechas/teléfonos:
<!-- en app/layout.tsx -->
<head>
<meta name="format-detection" content="telephone=no, date=no, email=no" />
</head>
Pro-tip: Diagnóstico rápido con console.log en SSR
Agrega esto temporalmente en el componente sospechoso:
if (typeof window === 'undefined') {
console.log('SERVER RENDER:', props);
} else {
console.log('CLIENT RENDER:', props);
}
Si los logs difieren en estructura o contenido, ahí está el problema.
⚠️ Nunca uses
suppressHydrationWarningen elementos contenedores (como<div>,<p>,<span>genéricos). Solo en elementos específicos con contenido no determinista (ej.<time>,<span>con timestamp).
Top comments (0)