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 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() o new 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 "..."
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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 />;
}
Enter fullscreen mode Exit fullscreen mode

✅ 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>
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

Si los logs difieren en estructura o contenido, ahí está el problema.

⚠️ Nunca uses suppressHydrationWarning en 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)