Cómo solucionar el error “Hydration failed because the initial UI does not match” en Next.js [Guía actualizada 2026]

スポンサーリンク

Cómo solucionar el error “Hydration failed because the initial UI does not match” en Next.js [Guía actualizada 2026]

Si estás desarrollando con Next.js y de repente aparece el error “Hydration failed because the initial UI does not match what was rendered on the server”, no estás solo. Este error es especialmente frecuente en entornos React 19 y Next.js 15 con App Router, con miles de comentarios en GitHub Discussions y constantemente en tendencia en Stack Overflow. Este artículo proporciona una guía completa actualizada a 2026 para identificar las causas y aplicar soluciones específicas, con ejemplos de código reales.

¿Qué es este error? Síntomas que experimentarás

Un Hydration Error (error de hidratación) en Next.js ocurre cuando el HTML renderizado en el servidor no coincide con el HTML que React intenta renderizar en el cliente durante el renderizado inicial.

Específicamente, los siguientes mensajes de error aparecerán en la consola de desarrollo del navegador:

Unhandled Runtime Error
Error: Hydration failed because the initial UI does not match what was rendered on the server.

También puedes ver estas variantes:

Error: Text content does not match server-rendered HTML.
Error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.

Cuando ocurre este error, Next.js descarta el resultado del renderizado del lado del servidor (SSR) y vuelve a renderizar toda la página en el cliente. Esto provoca varias consecuencias graves:

  • Degradación significativa del rendimiento: Se pierden todos los beneficios del SSR, haciendo la carga inicial más lenta
  • Impacto negativo en SEO: Los rastreadores de motores de búsqueda podrían no recibir el HTML correcto
  • Mala experiencia de usuario: Se produce un parpadeo de pantalla (flash de contenido)
  • Deterioro de la experiencia del desarrollador: La consola se llena de errores, facilitando que se pasen por alto otros problemas

En Next.js 15 combinado con React 19, las verificaciones de consistencia de hidratación se han vuelto más estrictas, y los casos que antes eran solo advertencias ahora se tratan como errores.

Causas de este error

Causa 1: Uso de APIs exclusivas del navegador (window / localStorage / document)

La causa más frecuente es referenciar directamente APIs exclusivas del navegador como window, localStorage y document durante el renderizado. Estos objetos no existen en el lado del servidor.

En el servidor, estos objetos son undefined. Por lo tanto, usar ramas condicionales como typeof window !== 'undefined' en la lógica de renderizado causa diferentes salidas entre servidor y cliente, provocando errores de hidratación.

// MAL: Esto causa un error de hidratación
function MyComponent() {
  const isClient = typeof window !== 'undefined';
  return <div>{isClient ? 'Cliente' : 'Servidor'}</div>;
}

Causa 2: Valores no deterministas como marcas de tiempo y números aleatorios

Usar funciones que devuelven valores diferentes cada vez que se llaman, como new Date() o Math.random(), durante el renderizado causa discrepancias entre la salida del servidor y del cliente.

// MAL: La marca de tiempo difiere entre servidor y cliente
function Clock() {
  return <p>Hora actual: {new Date().toLocaleTimeString()}</p>;
}

Siempre hay un desfase temporal entre cuando el servidor renderiza y cuando el cliente hidrata, por lo que la hora mostrada naturalmente diferirá, resultando en un error de discrepancia de contenido de texto.

Causa 3: Estructura de anidamiento HTML inválida

Estructuras de anidamiento inválidas que violan las especificaciones HTML (por ejemplo, colocar un <div> dentro de una etiqueta <p>) hacen que el parser HTML del navegador corrija automáticamente el DOM, creando una estructura DOM diferente del HTML renderizado en el servidor.

// MAL: <div> no puede colocarse dentro de <p>
function BadNesting() {
  return (
    <p>
      Texto
      <div>Este es un anidamiento inválido</div>
    </p>
  );
}

El navegador corrige automáticamente este HTML inválido, causando una discrepancia entre el HTML enviado por el servidor y el DOM del navegador.

Causa 4: Extensiones del navegador que modifican el DOM

Extensiones del navegador como Colorzilla y Grammarly pueden inyectar atributos o elementos HTML en el DOM de la página. Por ejemplo, Colorzilla agrega un atributo cz-shortcut-listen="true" a la etiqueta <body>. Esto ha sido reportado como particularmente problemático en entornos Next.js 15 + React 19.

Causa 5: Scripts de terceros e inconsistencias de contenido CMS

Scripts externos (etiquetas de publicidad, analíticas, etc.) que modifican el DOM, o contenido HTML obtenido de CMS que contiene estructuras de anidamiento inválidas, pueden provocar errores de hidratación.

Solución 1: Aislar la lógica exclusiva del cliente con el hook useEffect (Recomendado)

La solución más efectiva y recomendada es mover la lógica dependiente del navegador al hook useEffect y producir solo valores deterministas durante el renderizado inicial.

Paso 1: Implementar el patrón de gestión de estado del cliente

Primero, define un State con el mismo valor inicial para servidor y cliente, luego actualízalo al valor del lado del cliente con useEffect.

'use client';

import { useState, useEffect } from 'react';

function ThemeProvider({ children }) {
  // Usar un valor seguro para el servidor como estado inicial
  const [theme, setTheme] = useState('light');
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    // Solo acceder a localStorage en el cliente
    const savedTheme = localStorage.getItem('theme') || 'light';
    setTheme(savedTheme);
    setMounted(true);
  }, []);

  // Antes del montaje, devolver la misma salida que el servidor
  if (!mounted) {
    return <div data-theme="light">{children}</div>;
  }

  return <div data-theme={theme}>{children}</div>;
}

Paso 2: Establecer marcas de tiempo y valores aleatorios dentro de useEffect

'use client';

import { useState, useEffect } from 'react';

function Clock() {
  const [currentTime, setCurrentTime] = useState('');

  useEffect(() => {
    const updateTime = () => {
      setCurrentTime(new Date().toLocaleTimeString());
    };
    updateTime();
    const timer = setInterval(updateTime, 1000);
    return () => clearInterval(timer);
  }, []);

  // Mostrar cadena vacía en el renderizado inicial (coincide con el servidor)
  return <p>Hora actual: {currentTime || 'Cargando...'}</p>;
}

Paso 3: Hacerlo reutilizable con un hook personalizado

Si usas este patrón en varios componentes, extráelo a un hook personalizado.

'use client';

import { useState, useEffect } from 'react';

// Hook personalizado para gestionar el estado de montaje
function useHasMounted() {
  const [hasMounted, setHasMounted] = useState(false);
  useEffect(() => {
    setHasMounted(true);
  }, []);
  return hasMounted;
}

// Ejemplo de uso
function ClientOnlyComponent() {
  const hasMounted = useHasMounted();

  if (!hasMounted) {
    return <div>Cargando...</div>;
  }

  return <div>Ancho de ventana: {window.innerWidth}px</div>;
}

Notas importantes

  • useEffect no se ejecuta en el lado del servidor, por lo que las APIs del navegador se pueden usar de forma segura dentro de él
  • Muestra un placeholder o UI de respaldo durante el renderizado inicial para mantener la experiencia del usuario
  • Este patrón puede mostrar brevemente un placeholder, pero es mucho más ligero que el re-renderizado de toda la página causado por errores de hidratación

Solución 2: Deshabilitar SSR con next/dynamic

Para componentes difíciles de manejar con el patrón useEffect (por ejemplo, bibliotecas de terceros que dependen fuertemente de APIs del navegador), deshabilitar SSR completamente usando next/dynamic es efectivo.

import dynamic from 'next/dynamic';

// Importar con SSR deshabilitado
const MapComponent = dynamic(
  () => import('../components/Map'),
  {
    ssr: false,
    loading: () => <div className="map-placeholder">Cargando mapa...</div>
  }
);

// Ejemplo de uso
function LocationPage() {
  return (
    <div>
      <h1>Ubicación de la tienda</h1>
      <MapComponent />
    </div>
  );
}

Este método es especialmente efectivo en los siguientes casos:

  • Bibliotecas de mapas (Leaflet, Google Maps, etc.): Porque dependen del objeto window
  • Editores de texto enriquecido (Quill, TipTap, etc.): Porque manipulan directamente el objeto document
  • Bibliotecas de gráficos (Chart.js, D3.js, etc.): Porque realizan operaciones DOM de SVG o Canvas

Sin embargo, los componentes con SSR deshabilitado no se incluyen en el HTML inicial, así que evita esto para contenido crítico para SEO. La mejor práctica es limitar su uso a elementos interactivos que no sean contenido principal (mapas, gráficos, editores, etc.).

Solución 3: Corregir anidamiento HTML y uso apropiado de suppressHydrationWarning

Como solución avanzada, puedes corregir las estructuras de anidamiento HTML y usar suppressHydrationWarning para casos inevitables.

Corregir anidamiento HTML inválido

Primero, verifica si hay anidamiento HTML inválido. Aquí están los patrones inválidos comunes y sus correcciones:

// MAL: <div> dentro de <p>
<p>Texto<div>Elemento de bloque</div></p>

// BIEN: Cambiar a <div>
<div>Texto<div>Elemento de bloque</div></div>

// MAL: <a> dentro de <a>
<a href="/padre">
  Enlace padre
  <a href="/hijo">Enlace hijo</a>
</a>

// BIEN: Eliminar anidamiento
<div>
  <a href="/padre">Enlace padre</a>
  <a href="/hijo">Enlace hijo</a>
</div>

Uso limitado de suppressHydrationWarning

Para elementos pequeños donde las diferencias de valor entre servidor y cliente son inevitables (como visualización de marcas de tiempo), puedes usar suppressHydrationWarning.

// Usar solo cuando las diferencias son aceptables, como marcas de tiempo
<time suppressHydrationWarning>
  {new Date().toLocaleDateString()}
</time>

Nota importante: suppressHydrationWarning solo oculta el error; no resuelve el problema subyacente. Se aplica solo a una profundidad de nodos de texto, y las discrepancias en elementos hijos siguen siendo detectadas. Úsalo con moderación y solo cuando sea absolutamente necesario.

Manejo de extensiones del navegador

Si las extensiones del navegador son la causa, puedes agregar suppressHydrationWarning a la etiqueta <body>.

// app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html lang="es">
      <body suppressHydrationWarning>{children}</body>
    </html>
  );
}

Esto suprime las advertencias causadas por atributos que las extensiones agregan al body. Sin embargo, las discrepancias en los hijos directos del body siguen siendo detectadas.

Cómo prevenir este error

Para prevenir proactivamente los errores de hidratación, incorpora estas medidas preventivas en tu flujo de trabajo de desarrollo diario.

1. Mantener el renderizado inicial “determinista”

El principio más importante es garantizar una salida idéntica para los renderizados iniciales del servidor y del cliente. Los valores dependientes del entorno (APIs del navegador, marcas de tiempo, números aleatorios, etc.) deben colocarse siempre dentro de useEffect.

2. Separar claramente Server Components y Client Components

En el App Router de Next.js 15, los componentes son Server Components por defecto. Cuando uses APIs del navegador o hooks de React (useState, useEffect, etc.), siempre agrega la directiva 'use client' al inicio del archivo.

3. Aprovechar las reglas de ESLint

eslint-plugin-react incluye reglas para detectar anidamiento HTML inválido (como jsx-no-invalid-html-nesting). Intégralas en tu pipeline de CI para verificaciones automáticas.

4. Pruebas regulares

Prueba periódicamente en modo incógnito (navegación privada) o con extensiones del navegador deshabilitadas para aislar errores relacionados con extensiones.

5. Diseño responsivo basado en CSS

En lugar de bifurcar el marcado según el tamaño del viewport, usa media queries CSS o display: none para alternar la visibilidad, manteniendo la estructura HTML consistente entre servidor y cliente.

// MAL: Bifurcación por viewport en JS
{isMobile ? <MobileNav /> : <DesktopNav />}

// BIEN: Alternancia basada en CSS
<MobileNav className="block md:hidden" />
<DesktopNav className="hidden md:block" />

Resumen

El Hydration Error de Next.js “Hydration failed because the initial UI does not match what was rendered on the server” ocurre debido a discrepancias entre SSR/SSG y el renderizado del lado del cliente, siendo uno de los errores más frecuentes en el desarrollo con Next.js.

Puntos clave:

  • El problema central es “resultados de renderizado inicial diferentes entre servidor y cliente”
  • Causas más comunes: uso directo de APIs exclusivas del navegador, renderizado de valores no deterministas, anidamiento HTML inválido
  • Soluciones recomendadas: renderizado por etapas con useEffect, deshabilitar SSR con next/dynamic cuando sea necesario
  • suppressHydrationWarning debe usarse como último recurso, con alcance limitado
  • Prevención: mantener renderizados iniciales deterministas, separar claramente Server/Client Components

Si estas soluciones no resuelven el problema, verifica lo siguiente:

  1. Actualiza Next.js y React a las últimas versiones
  2. Elimina las carpetas node_modules y .next y reconstruye
  3. Busca casos similares en GitHub Discussions de Next.js (https://github.com/vercel/next.js/discussions)
  4. Crea una reproducción mínima y repórtala como Issue

Referencias

コメント

タイトルとURLをコピーしました