{
  "id": "ai-creates-hydration-mismatches",
  "type": "failures",
  "category": "failures",
  "locale": "zh",
  "url": "/zh/failures/ai-creates-hydration-mismatches",
  "title": "如何修复AI导致的React水合不匹配",
  "description": "AI代理生成的React组件在服务器端和客户端渲染出不同的HTML，导致首次加载时出现水合错误和界面损坏。",
  "tools": [
    "Cursor",
    "Claude Code",
    "Codex",
    "Windsurf"
  ],
  "stack": [
    "Next.js",
    "Astro",
    "TypeScript"
  ],
  "tags": [
    "hydration",
    "react",
    "nextjs"
  ],
  "difficulty": null,
  "updated": "2026-06-08",
  "markdown": "代理编写的组件在服务器端和客户端产生不同的HTML——日期、随机值、`window`检查——导致React抛出水合不匹配错误并在加载时重新渲染整个树。\n\n## 症状\n\n组件在渲染过程中读取`Date.now()`、`Math.random()`或`window`，导致服务器端和客户端输出不同。\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\n浏览器控制台显示：\n\n```\nError: Hydration failed because the initial UI does not match what was rendered\non the server.\n```\n\n## 原因\n\n代理编写的组件单独看似乎正确。但它没有模拟两次独立的渲染过程（SSR + 客户端水合），因此不知道两者之间任何不同的值都会破坏水合。\n\n## 如何识别\n\n- 直接在JSX或渲染过程中调用`Date.now()`、`new Date()`、`Math.random()`，而没有放在`useEffect`或`useState`初始化器中。\n- 在渲染返回中使用`typeof window !== \"undefined\"`守卫——服务器始终走`false`分支。\n- 在渲染过程中读取`localStorage`、`sessionStorage`或`navigator`。\n- 服务器和客户端输出之间HTML标签或属性值不匹配（在React DevTools的水合警告中可见）。\n\n## 如何修复\n\n将仅浏览器可用的值延迟到挂载后，或者在服务器端生成稳定值并通过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## 修复提示\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## 测试\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```"
}