{
  "id": "ai-overuses-useeffect",
  "type": "failures",
  "category": "failures",
  "locale": "zh",
  "url": "/zh/failures/ai-overuses-useeffect",
  "title": "如何修复AI过度使用useEffect的问题",
  "description": "AI代理经常使用useEffect来处理派生状态、事件转换和数据获取，而这些本应使用useMemo、事件处理器或Server Components。",
  "tools": [
    "Cursor",
    "Claude Code",
    "Codex",
    "Windsurf"
  ],
  "stack": [
    "Next.js",
    "TypeScript"
  ],
  "tags": [
    "react",
    "nextjs",
    "hydration"
  ],
  "difficulty": null,
  "updated": "2026-06-08",
  "markdown": "代理将本应作为派生状态、事件处理器或Server Component获取的逻辑包装在`useEffect`中，从而导致额外渲染、闭包过时和竞态条件。\n\n## 症状\n\n`useEffect`被用于计算派生值或同步状态，而这些本可以简单计算得出。\n\n```tsx\n\"use client\";\nimport { useState, useEffect } from \"react\";\n\n// WRONG — useEffect to compute derived state\nexport function CartSummary({ items }: { items: CartItem[] }) {\n  const [total, setTotal] = useState(0);\n\n  useEffect(() => {\n    setTotal(items.reduce((sum, item) => sum + item.price * item.qty, 0));\n  }, [items]);\n\n  return <p>Total: ${total}</p>;\n  // Renders twice: once with total=0, once after the effect runs\n}\n```\n\n另一种模式：在`useEffect`中获取数据，而Server Component可以无需任何客户端包开销完成。\n\n```tsx\n\"use client\";\nuseEffect(() => {\n  fetch(\"/api/products\").then((r) => r.json()).then(setProducts);\n}, []);\n// Waterfall: page loads -> JS executes -> fetch starts -> render\n```\n\n## 原因\n\n`useEffect`是React 16/17中主要的副作用出口。基于React 18之前和App Router之前代码训练的模型默认学会了使用它。“我需要渲染后发生某些事情”的心智模型映射到`useEffect`，即使真正的修复更简单。\n\n## 如何识别\n\n- `useEffect`设置了可以直接从props或其他状态计算得出的状态。\n- 在不在Server Component子树中的组件顶部使用`useEffect(() => { fetch(...).then(setState) }, [])`。\n- `useEffect`没有清理函数，且依赖项是一个函数（闭包过时风险）。\n- `useEffect`仅用于挂载时的日志或分析。\n\n## 如何修复\n\n使用正确的工具：\n\n```tsx\n// CORRECT — derived state: just compute it\nexport function CartSummary({ items }: { items: CartItem[] }) {\n  const total = items.reduce((sum, item) => sum + item.price * item.qty, 0);\n  return <p>Total: ${total.toFixed(2)}</p>;\n}\n\n// CORRECT — expensive derivation: useMemo\nimport { useMemo } from \"react\";\n\nexport function FilteredList({ items, query }: { items: Item[]; query: string }) {\n  const filtered = useMemo(\n    () => items.filter((i) => i.name.toLowerCase().includes(query.toLowerCase())),\n    [items, query]\n  );\n  return <ul>{filtered.map((i) => <li key={i.id}>{i.name}</li>)}</ul>;\n}\n```\n\n```tsx\n// CORRECT — data fetching: async Server Component (no useEffect needed)\n// app/products/page.tsx\nexport default async function ProductsPage() {\n  const products = await fetch(\"https://api.example.com/products\").then((r) =>\n    r.json()\n  );\n  return <ProductList products={products} />;\n}\n```\n\n```txt\n[ ] Derived values from props/state are plain calculations, not useEffect + setState\n[ ] Expensive derivations use useMemo, not useEffect\n[ ] Data fetching on mount moves to async Server Components or React Query/SWR\n[ ] Event-driven side-effects (form submit, button click) are in event handlers\n[ ] useEffect is reserved for: external system sync, subscriptions, cleanup\n[ ] Every useEffect that sets state has a loading/error state and cleanup\n```\n\n## 修复提示\n\n```txt title=\"Fix Prompt\"\nThis component uses useEffect to compute derived state or fetch initial data.\nRefactor it: replace derived-state effects with direct calculations or useMemo,\nmove initial data fetching to an async Server Component parent or to a\ndata-fetching library (SWR/React Query) with proper loading/error handling, and\nkeep useEffect only for genuine external-system synchronization that requires\ncleanup. Explain each useEffect you keep and why it cannot be replaced.\n```\n\n## 测试\n\n```bash\n# Count useEffect calls — a high number is a smell worth reviewing\ngrep -rn \"useEffect\" --include=\"*.tsx\" --include=\"*.ts\" src/ app/ \\\n  | grep -v \"node_modules\" \\\n  | wc -l\n\n# Flag useEffect+setState patterns for manual review\ngrep -A5 \"useEffect\" app/**/*.tsx 2>/dev/null | grep \"setState\\|set[A-Z]\" \\\n  && echo \"REVIEW: possible derived-state antipattern\" || echo \"OK\"\n```"
}