# Cómo solucionar los errores de hidratación en React causados por IA

> Los agentes de IA generan componentes de React que renderizan HTML diferente en el servidor y el cliente, lo que provoca errores de hidratación y una interfaz rota en la primera carga.

**Type:** Failure  
**Tools:** Cursor, Claude Code, Codex, Windsurf  
**Stack:** Next.js, Astro, TypeScript  
**Updated:** 2026-06-08

---

El agente escribe componentes que producen HTML diferente en el servidor y el
cliente — fechas, valores aleatorios, comprobaciones de `window` — lo que hace que React lance un
error de hidratación y vuelva a renderizar todo el árbol al cargar.

## El síntoma

Un componente lee `Date.now()`, `Math.random()` o `window` durante el renderizado,
produciendo una salida diferente en el servidor y en el cliente.

```tsx
// WRONG — hydration mismatch
export function Timestamp() {
  return <p>Page loaded at: {new Date().toLocaleTimeString()}</p>;
  // Server renders "10:00:00 AM", client renders "10:00:01 AM" — mismatch
}

// WRONG — conditional on window
export function ThemeIcon() {
  const isDark = typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches;
  return <span>{isDark ? "🌙" : "☀️"}</span>;
  // Server: always renders "☀️"; client: may render "🌙" — mismatch
}
```

La consola del navegador muestra:

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

## Por qué ocurre

El agente escribe componentes que parecen correctos de forma aislada. No simula
dos pases de renderizado separados (SSR + hidratación del cliente) y no sabe que cualquier
valor que difiera entre ambos romperá la hidratación.

## Cómo detectarlo

- `Date.now()`, `new Date()`, `Math.random()` llamados directamente en JSX o durante
  el renderizado sin estar dentro de un inicializador de `useEffect` o `useState`.
- Guardias `typeof window !== "undefined"` dentro del retorno del renderizado — el servidor
  siempre toma la rama `false`.
- Lectura de `localStorage`, `sessionStorage` o `navigator` durante el renderizado.
- Etiquetas HTML o valores de atributos diferentes entre la salida del servidor y el cliente
  (visible en la advertencia de hidratación de React DevTools).

## Cómo solucionarlo

Diferir los valores solo del navegador hasta después del montaje, o generar valores estables en el servidor
y pasarlos como props.

```tsx
// CORRECT — defer client-only value to after mount
"use client";
import { useState, useEffect } from "react";

export function Timestamp() {
  const [time, setTime] = useState<string | null>(null);

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

  if (!time) return <p>Loading time…</p>; // matches server output
  return <p>Page loaded at: {time}</p>;
}
```

```tsx
// CORRECT — theme icon: use CSS / data attribute, avoid JS render branch
// Set data-theme on <html> in a blocking inline script (not React)
// Then use CSS: [data-theme="dark"] .icon { content: "🌙" }

// Or use Next.js next-themes which handles SSR safely:
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";

export function ThemeIcon() {
  const { resolvedTheme } = useTheme();
  const [mounted, setMounted] = useState(false);
  useEffect(() => setMounted(true), []);
  if (!mounted) return <span style={{ width: 24 }} />; // stable placeholder
  return <span>{resolvedTheme === "dark" ? "🌙" : "☀️"}</span>;
}
```

```txt
[ ] No Date.now() / Math.random() / new Date() called directly during render
[ ] No typeof window / localStorage / navigator accessed during render
[ ] Browser-only state initializes to null/undefined, set in useEffect
[ ] Render a stable placeholder (same as server output) until after mount
[ ] Use suppressHydrationWarning only on elements where the mismatch is intentional (e.g. timestamp)
[ ] Run "next build && next start" and check browser console for hydration errors
```

## Prompt de corrección

```txt title="Fix Prompt"
This component causes a React hydration mismatch because it reads browser-only
values (Date, window, localStorage, Math.random) during render. Fix it: move
all browser-only reads into a useEffect that sets state after mount, render a
stable placeholder on first render that matches what the server produces, and
never conditionally return different JSX trees based on typeof window. Explain
why each change prevents the mismatch.
```

## Prueba

```bash
# Build and start, then check for hydration errors with curl diff
next build 2>&1 | grep -i "hydrat\|mismatch" && echo "FAIL: hydration error in build" || echo "Build OK"

# Runtime: open browser console after next start and look for hydration warnings
next start &
sleep 3
curl -s http://localhost:3000 | grep -i "data-nextjs-scroll-focus-boundary" > /dev/null && echo "Server rendered OK"
```