DEV Community

Erick Eduardo Ramos
Erick Eduardo Ramos

Posted on

Cómo solucionar \"Text content does not match server-rendered HTML\" en Next.js App Router

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-components sin @emotion/react en 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

  1. 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
  1. Busca el componente afectado: el error suele apuntar al nodo DOM donde ocurre la discrepancia (ej. <span>, <time>, etc.).
  2. 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

  1. Reinicia el dev server: npm run dev
  2. Forza una limpieza de cache: rm -rf .next && npm run dev
  3. Prueba en modo incógnito (para descartar extensiones como "React DevTools" o "AdBlock")
  4. 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 useEffect o useActionState.
  • Evita funciones no puras en render: Math.random(), Date.now(), fetch() (usa fetch en Server Components o useSWR).
  • Valida tu HTML con w3c-validator para 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 suppressHydrationWarning o retrasarse con useEffect.

Con esto, el error desaparecerá de forma definitiva.

Top comments (0)