Cómo solucionar el error "Text content does not match server-rendered HTML" en Next.js App Router
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 el árbol de React que se construye durante la primera renderización en el navegador (hydration). Es un problema crítico de consistencia de estado que rompe la experiencia de usuario y puede causar comportamientos impredecibles. 🔍 Causa raíz (diagnóstico técnico) En tu caso, el error está relacionado con contenido dinámico que varía entre renderizado del servidor y renderizado del cliente, probablemente causado por: - Uso de Date() ,Math.random() ,localStorage ,window , o APIs del navegador directamente en el render. - Uso de typeof window !== 'undefined' como condición de renderizado (no es idempotente entre SSR y CSR). - Metaetiquetas de detección automática de iOS ( format-detection ) que inyectan nodos en tiempo de ejecución. - Extensiones del navegador (especialmente en desarrollo) que modifican el DOM. - Librerías CSS-in-JS mal configuradas que inyectan clases o estilos dinámicos en CSR. ⚠️ Nota crítica: Next.js App Router no permite el uso de useEffect para evitar el mismatch en el primer render - el mismatch debe prevenirse, no suprimirse. ✅ Solución definitiva (pasos verificados) Paso 1: Elimina toda lógica no determinista del render NUNCA uses lo siguiente directamente en el cuerpo del componente: // ❌ Evitar const now = new Date(); // ❌ const isClient = typeof window !== 'undefined'; // ❌ const randomId = Math.random(); // ❌ const theme = localStorage.getItem('theme'); // ❌ ✅ Reemplaza con: // ✅ Usar useEffect para actualizar el estado, no para determinar el render inicial import { useState, useEffect } from 'react'; export default function Component() { const [time, setTime] = useState (''); // Inicializa con valor seguro (ej. string vacío o placeholder) useEffect(() => { setTime(new Date().toISOString()); }, []); return {time || 'Cargando...'} ; } ✅ Clave: El servidor y el cliente renderizan exactamente lo mismo en el primer render (ej. '' o'Cargando...' ). El cambio ocurre después de hydration, cuando React ya ha montado el componente. Paso 2: Deshabilita la detección automática de iOS (si aplica) Agrega esta metaetiqueta en (en app/layout.tsx ): // app/layout.tsx export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( {children} ); } ✅ Esto evita que iOS inyecte tags alrededor de fechas/números, causando mismatches silenciosos. Paso 3: Usa suppressHydrationWarning solo como escape hatch Solo si el contenido debe cambiar (ej. reloj en tiempo real, UUIDs): // ✅ Solo para contenido intencionalmente no determinista {typeof window !== 'undefined' ? window.innerWidth : 0} ⚠️ Restricciones: - Solo funciona en el nodo inmediato (no en hijos anidados). - React ignora por completo mismatches en ese subárbol. - Nunca lo uses en elementos interactivos ( , , etc.). Paso 4: Verifica configuraciones de Edge/CDN Si usas Cloudflare, Vercel Edge Functions, o Akamai: - Desactiva Auto Minify (Cloudflare) o HTML Minification (Vercel). - Asegúrate de que el servidor no modifique el HTML (ej. middlewares que inyecten scripts o cookies). 🧪 Diagnóstico rápido (checklist) | Síntoma | Causa probable | Solución | |---|---|---| Expected server HTML ... does not match ... | Uso de Date() en render | Inicializa con '' + useEffect | Mismatch en o anidados | iOS inyecta en fechas/teléfonos | Metaetiqueta format-detection | | Error solo en desarrollo | Extensiones (ej. React DevTools, Dark Mode) | Prueba en incógnito | | Error en producción | CDN/Edge modificando HTML | Desactivar minificación HTML | 💡 Pro-tip: Prueba de "hydration safety" Antes de desplegar, ejecuta este script en consola en producción (sin React DevTools): document.body.innerHTML = document.body.innerHTML; Si no hay errores en consola, tu render es hydration-safe. Si sí hay errores, busca el primer console.error con Text content does not match... - ahí está tu fuente. 📦 Código corregido (ejemplo real) // app/page.tsx import { useState, useEffect } from 'react'; export default function Page() { const [version, setVersion] = useState ('Cargando...'); useEffect(() => { // Solo se ejecuta en cliente → no causa mismatch setVersion('16.3.2'); // o fetch('.../version') }, []); return ( Latest Version {version} ); } ✅ Resultado: - Servidor renderiza 'Cargando...' - Cliente renderiza 'Cargando...' → luego actualiza a'16.3.2' - Zero mismatch. 🔥 Consejo final: Si el error persiste, usa console.log('SERVER:', process.env.NEXT_PUBLIC_VERCEL_ENV) para verificar si el código se ejecuta en SSR. Si lo hace, nada de lógica del navegador debe estar ahí. Top comments (0)
Comments
No comments yet. Start the discussion.