Back

Errores de Hidratación en Next.js: Guía de Supervivencia (Adiós Warning)

Si desarrollas con Next.js, seguro que te has topado con este mensajito en rojo sangre que inunda tu consola.

Error: Text content does not match server-rendered HTML.
Warning: Prop className did not match. Server: "bg-blue-500" Client: "bg-red-500"

Al principio lo ignoras. "Bah, si se ve bien".
Pero cuidado (¡Ojo!). Este error no es solo ruido. Significa que tu web está sufriendo un Layout Shift (el contenido baila) o, peor aún, que tus botones y formularios podrían no responder. Esencialmente, React está teniendo una crisis de identidad.

Hoy no vamos a poner parches. Vamos a entender qué demonios es la "Hidratación" y cómo arreglar estos errores como un Senior.

¿Qué es eso de la "Hidratación"?

Para matar al bug, hay que entender cómo funciona la bestia.

En los viejos tiempos (Create React App), el navegador recibía un <body> vacío y luego JS pintaba todo.
En Next.js (SSR), la película es diferente:

  1. Server: React corre en el backend y genera el HTML listo (<h1>Hola</h1>).
  2. Viaje: Ese HTML llega a tu navegador. El usuario ve el texto al instante (¡Rapidez!).
  3. Hidratación: React se despierta en el navegador. Pero no vuelve a pintar. Simplemente se "engancha" al HTML que ya existe para darle vida (eventos, estado).

La Regla de Oro:

Lo que el servidor pintó y lo que el cliente cree que debe pintar, tienen que ser idénticos.

Si el servidor dijo "Hola A" y tu navegador calcula "Hola B", React entra en pánico.
"¿Pero qué es esto? ¡No confío en este DOM!"
Y ahí es cuando explota todo.

Los 4 Culpables de Siempre

El 99% de las veces, es una de estas cosas.

1. El Viajero del Tiempo (Timestamps)

El clásico de los clásicos.

export default function Footer() { // 💣 ¡BOOM! return <footer>Hora: {new Date().toLocaleTimeString()}</footer>; }
  • Servidor: Generó el HTML a las 10:00:00.
  • Cliente: Ejecutó el JS a las 10:00:01.

Diferentes horas = Diferente HTML = Error de Hidratación.
Usar Math.random() directamente en el render también causa esto.

2. HTML "Ilegal" (Anidamiento Incorrecto)

El navegador es buena gente y te corrige el HTML. React es un policía estricto.

Lo que nunca debes hacer: Meter un <div> dentro de un <p>.

// ❌ Jamás hagas esto <p> Hola <div>Mundo</div> </p>

Según la especificación HTML, un párrafo (<p>) no puede tener bloques dentro.
El navegador, al ver esto, cierra el <p> antes del <div>.
Pero React (Virtual DOM) sigue pensando que el <div> está dentro. Desajuste total.

Solución: Usa <div> o <span> en lugar de <p>.

3. Las Extensiones "Metomentodo"

A veces tu código es perfecto, pero el error sigue.
¿Tienes instalado Grammarly? ¿Un traductor? ¿Password Manager?

Estas extensiones manipulan el DOM inyectando sus propios iconos o spans antes de que React termine de hidratar.
React se encuentra con elementos que él no creó y se queja.

Si abres la web en Modo Incógnito y el error desaparece, no es culpa tuya (es de la extensión).

4. Usar window a lo loco

export default function Navbar() { // En Node.js (Servidor) no existe 'window'! const isMobile = window.innerWidth < 768; return <nav>{isMobile ? 'Menú' : 'Navbar Completo'}</nav>; }

Mucha gente hace el truco de:

const isMobile = typeof window !== 'undefined' && window.innerWidth < 768; // Server: false (window undefined) -> Pinta Desktop // Cliente: true -> Pinta Mobile

¡Error! El servidor mandó el menú de escritorio, pero el móvil intenta pintar el menú hamburguesa. El HTML no coincide.

Soluciones: Cómo Arreglarlo de Verdad

Estrategia 1: useEffect al rescate (La forma segura)

Si algo depende del navegador (como window o localStorage), dile a React que espere.

// hooks/useIsMounted.ts import { useState, useEffect } from 'react'; export function useIsMounted() { const [mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); }, []); return mounted; } // En tu componente function Reloj() { const isMounted = useIsMounted(); // 1. Si no está montado (Server), no pintes nada (o un loader) if (!isMounted) return null; // 2. Solo pinta la hora cuando estemos en el cliente return <span>{new Date().toLocaleTimeString()}</span>; }

Es un poco más de código, pero te asegura 0 errores.

Estrategia 2: suppressHydrationWarning (El parche rápido)

"Mira React, sé que la hora va a ser distinta. No me molestes."

<span suppressHydrationWarning> {new Date().toLocaleTimeString()} </span>

Esto hace que React ignore las diferencias de texto en ese elemento.
¡Ojo!: Solo funciona para texto. No lo pongas en el <body> ni esperes que arregle componentes enteros distintos.

Estrategia 3: dynamic import (Solo Next.js)

Si usas una librería pesada que rompe todo (como mapas o editores), sácala del SSR.

import dynamic from 'next/dynamic'; // "Carga esto solo en el cliente, por favor" const Mapa = dynamic(() => import('./Mapa'), { ssr: false, // La magia loading: () => <p>Cargando mapa...</p>, }); export default function Page() { return <Mapa />; }

Next.js mandará el "Cargando..." desde el servidor, y el mapa solo se generará en el navegador. Santo remedio.

Resumen

Cuando veas el error rojo, respira y revisa:

  1. ¿HTML Inválido?: ¿Un div dentro de un p?
  2. ¿Cosas del Browser?: ¿Usas window, localStorage o fechas fuera de un useEffect?
  3. ¿Extensiones?: Prueba en incógnito.

La hidratación es el precio que pagamos por tener webs ultra-rápidas con SSR. Domina estos trucos y dejarás de pelearte con la consola.

¡A codear! 👨‍💻👩‍💻

Next.jsReactDebuggingWeb DevelopmentHydration