DEV Community

Erick Eduardo Ramos
Erick Eduardo Ramos

Posted on

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 <a> 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'); // ❌
Enter fullscreen mode Exit fullscreen mode

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<string>(''); // Inicializa con valor seguro (ej. string vacío o placeholder)

  useEffect(() => {
    setTime(new Date().toISOString());
  }, []);

  return <time dateTime={time || ''} suppressHydrationWarning>{time || 'Cargando...'}</time>;
}
Enter fullscreen mode Exit fullscreen mode

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 <head> (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

✅ Esto evita que iOS inyecte <a> 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
<div suppressHydrationWarning>
  {typeof window !== 'undefined' ? window.innerWidth : 0}
</div>
Enter fullscreen mode Exit fullscreen mode

⚠️ 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 (<button>, <a>, 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 <p> o <a> anidados iOS inyecta <a> 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;
Enter fullscreen mode Exit fullscreen mode

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<string>('Cargando...');

  useEffect(() => {
    // Solo se ejecuta en cliente → no causa mismatch
    setVersion('16.3.2'); // o fetch('.../version')
  }, []);

  return (
    <div>
      <h1>Latest Version</h1>
      <span suppressHydrationWarning>{version}</span>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

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)