{
  "id": "ai-creates-hydration-mismatches",
  "type": "failures",
  "category": "failures",
  "locale": "en",
  "url": "/failures/ai-creates-hydration-mismatches",
  "title": "How to Fix AI Creating React Hydration Mismatches",
  "description": "AI agents generate React components that render different HTML on the server and client, triggering hydration errors and broken UI on first load.",
  "tools": [
    "Cursor",
    "Claude Code",
    "Codex",
    "Windsurf"
  ],
  "stack": [
    "Next.js",
    "Astro",
    "TypeScript"
  ],
  "tags": [
    "hydration",
    "react",
    "nextjs"
  ],
  "difficulty": null,
  "updated": "2026-06-08",
  "markdown": "The agent writes components that produce different HTML on the server versus the\nclient — dates, random values, `window` checks — causing React to throw a\nhydration mismatch error and re-render the entire tree on load.\n\n## The symptom\n\nA component reads `Date.now()`, `Math.random()`, or `window` during render,\nproducing different output server-side vs. client-side.\n\n```tsx\n// WRONG — hydration mismatch\nexport function Timestamp() {\n  return <p>Page loaded at: {new Date().toLocaleTimeString()}</p>;\n  // Server renders \"10:00:00 AM\", client renders \"10:00:01 AM\" — mismatch\n}\n\n// WRONG — conditional on window\nexport function ThemeIcon() {\n  const isDark = typeof window !== \"undefined\" && window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n  return <span>{isDark ? \"🌙\" : \"☀️\"}</span>;\n  // Server: always renders \"☀️\"; client: may render \"🌙\" — mismatch\n}\n```\n\nThe browser console shows:\n\n```\nError: Hydration failed because the initial UI does not match what was rendered\non the server.\n```\n\n## Why it happens\n\nThe agent writes components that seem correct in isolation. It doesn't simulate\ntwo separate render passes (SSR + client hydration) and doesn't know that any\nvalue that differs between the two will break hydration.\n\n## How to spot it\n\n- `Date.now()`, `new Date()`, `Math.random()` called directly in JSX or during\n  render without being inside a `useEffect` or `useState` initializer.\n- `typeof window !== \"undefined\"` guards inside the render return — the server\n  always takes the `false` branch.\n- `localStorage`, `sessionStorage`, or `navigator` read during render.\n- Mismatched HTML tags or attribute values between server and client output\n  (visible in the React DevTools hydration warning).\n\n## How to fix it\n\nDefer browser-only values to after mount, or generate stable values server-side\nand pass them as props.\n\n```tsx\n// CORRECT — defer client-only value to after mount\n\"use client\";\nimport { useState, useEffect } from \"react\";\n\nexport function Timestamp() {\n  const [time, setTime] = useState<string | null>(null);\n\n  useEffect(() => {\n    setTime(new Date().toLocaleTimeString());\n  }, []);\n\n  if (!time) return <p>Loading time…</p>; // matches server output\n  return <p>Page loaded at: {time}</p>;\n}\n```\n\n```tsx\n// CORRECT — theme icon: use CSS / data attribute, avoid JS render branch\n// Set data-theme on <html> in a blocking inline script (not React)\n// Then use CSS: [data-theme=\"dark\"] .icon { content: \"🌙\" }\n\n// Or use Next.js next-themes which handles SSR safely:\nimport { useTheme } from \"next-themes\";\nimport { useEffect, useState } from \"react\";\n\nexport function ThemeIcon() {\n  const { resolvedTheme } = useTheme();\n  const [mounted, setMounted] = useState(false);\n  useEffect(() => setMounted(true), []);\n  if (!mounted) return <span style={{ width: 24 }} />; // stable placeholder\n  return <span>{resolvedTheme === \"dark\" ? \"🌙\" : \"☀️\"}</span>;\n}\n```\n\n```txt\n[ ] No Date.now() / Math.random() / new Date() called directly during render\n[ ] No typeof window / localStorage / navigator accessed during render\n[ ] Browser-only state initializes to null/undefined, set in useEffect\n[ ] Render a stable placeholder (same as server output) until after mount\n[ ] Use suppressHydrationWarning only on elements where the mismatch is intentional (e.g. timestamp)\n[ ] Run \"next build && next start\" and check browser console for hydration errors\n```\n\n## Fix Prompt\n\n```txt title=\"Fix Prompt\"\nThis component causes a React hydration mismatch because it reads browser-only\nvalues (Date, window, localStorage, Math.random) during render. Fix it: move\nall browser-only reads into a useEffect that sets state after mount, render a\nstable placeholder on first render that matches what the server produces, and\nnever conditionally return different JSX trees based on typeof window. Explain\nwhy each change prevents the mismatch.\n```\n\n## Test\n\n```bash\n# Build and start, then check for hydration errors with curl diff\nnext build 2>&1 | grep -i \"hydrat\\|mismatch\" && echo \"FAIL: hydration error in build\" || echo \"Build OK\"\n\n# Runtime: open browser console after next start and look for hydration warnings\nnext start &\nsleep 3\ncurl -s http://localhost:3000 | grep -i \"data-nextjs-scroll-focus-boundary\" > /dev/null && echo \"Server rendered OK\"\n```"
}