第01章:AI生成的useEffect依赖数组写错了

第01章:AI生成的useEffect依赖数组写错了

“AI 帮你写了一个 useEffect,里面用了5个变量,但 dependencies 数组只写了2个。ESLint 报警告,你不知道是不是可以忽略,就加了 // eslint-disable-next-line 注释。几天后出了一个 Bug——页面数据不刷新,或者相反,陷入无限请求循环。这两个问题都来自同一个根因:useEffect 依赖数组写错了。”


ℹ️ 版本说明:本章基于 React 19.2.6

1.1 AI默认会生成什么

// AI 通常给你的代码(依赖数组不完整)
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);
  
  useEffect(() => {
    fetchUser(userId).then(setUser);
    fetchPosts(userId, sortBy).then(setPosts);  // sortBy 没有在依赖里!
  }, [userId]);  // ← 缺少 sortBy
}

// 或者 AI 帮你"解决"警告的方式:禁用 ESLint
useEffect(() => {
  doSomething(a, b, c);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);  // ← 永远不更新

1.2 AI通常遗漏的4个坑

⚠️ 坑1:依赖遗漏 → 数据陈旧(Stale Closure)

// 问题:sortBy 是外部变量,变了也不触发 effect
function PostList({ userId }) {
  const [sortBy, setSortBy] = useState('date');
  const [posts, setPosts] = useState([]);
  
  useEffect(() => {
    // 这里的 sortBy 是 effect 创建时的值
    // 即使 sortBy 变了,这个 effect 不会重新执行
    fetchPosts(userId, sortBy).then(setPosts);
  }, [userId]);  // ← 缺少 sortBy!

  return (
    <>
      <button onClick={() => setSortBy('likes')}>按点赞排序</button>
      {/* 点击后 sortBy 变了,但 posts 不会更新! */}
    </>
  );
}

// 修复:依赖数组要完整
useEffect(() => {
  fetchPosts(userId, sortBy).then(setPosts);
}, [userId, sortBy]);  // ← 完整依赖

⚠️ 坑2:依赖是对象或函数 → 无限循环

// 问题:每次渲染都创建新的对象/函数
function UserList() {
  const filters = { active: true, role: 'admin' };  // 每次渲染新对象!
  
  useEffect(() => {
    fetchUsers(filters).then(setUsers);
  }, [filters]);  // filters 每次渲染都变 → 无限循环!

  // 修复方案1:把对象移到 useEffect 内部
  useEffect(() => {
    const filters = { active: true, role: 'admin' };  // 在 effect 内部定义
    fetchUsers(filters).then(setUsers);
  }, []);  // 不需要依赖
  
  // 修复方案2:用 useMemo 稳定引用(如果 filters 来自 state/props)
  const filters = useMemo(() => ({ active, role }), [active, role]);
  useEffect(() => {
    fetchUsers(filters).then(setUsers);
  }, [filters]);  // filters 只在 active/role 变时才变
}

// 函数依赖同理:
// 每次渲染都创建新函数 → 无限循环
const fetchData = () => fetch('/api/data');  // 新函数!
useEffect(() => fetchData(), [fetchData]);   // 无限循环
// 修复:useCallback 或把函数移进 effect

⚠️ 坑3:不该用 useEffect 的场景

// ❌ 这些场景不需要 useEffect:

// 场景1:根据 props 计算派生数据
function Component({ items }) {
  const [sorted, setSorted] = useState([]);
  useEffect(() => {                          // ❌ 不需要 effect
    setSorted([...items].sort());
  }, [items]);
  
  // ✅ 直接在渲染时计算
  const sorted = useMemo(() => [...items].sort(), [items]);
}

// 场景2:响应用户事件
function Form() {
  const [value, setValue] = useState('');
  const [result, setResult] = useState(null);
  useEffect(() => {                          // ❌ 不需要 effect
    if (value) validateInput(value).then(setResult);
  }, [value]);
  
  // ✅ 在事件处理里处理
  const handleChange = async (e) => {
    setValue(e.target.value);
    if (e.target.value) {
      const result = await validateInput(e.target.value);
      setResult(result);
    }
  };
}

// 场景3:同步通知父组件
function Child({ onDataChange }) {
  const [data, setData] = useState(null);
  useEffect(() => {                          // ❌ 不需要 effect,会额外渲染一次
    onDataChange(data);
  }, [data, onDataChange]);
  
  // ✅ 在修改 data 的同时通知父组件
  const updateData = (newData) => {
    setData(newData);
    onDataChange(newData);  // 直接调用
  };
}

⚠️ 坑4:React 19 新方案:use() 替代 useEffect 数据获取

// React 19 之前:useEffect + useState 组合(容易出错)
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    setLoading(true);
    fetchUser(userId)
      .then(setUser)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [userId]);  // 如果这里漏了 userId 就是 Bug
  
  if (loading) return <Spinner />;
  if (error) return <Error error={error} />;
  return <div>{user?.name}</div>;
}

// React 19 新方案:use() hook(更简洁,无法出现依赖错误)
import { use, Suspense } from 'react';

function UserProfile({ userId }) {
  // use() 接受 Promise,自动与 Suspense 集成
  const user = use(fetchUser(userId));  // ← 没有依赖数组,没有 loading/error 状态!
  return <div>{user.name}</div>;
}

// 父组件用 Suspense 和 ErrorBoundary 包裹
function App() {
  return (
    <ErrorBoundary fallback={<Error />}>
      <Suspense fallback={<Spinner />}>
        <UserProfile userId={123} />
      </Suspense>
    </ErrorBoundary>
  );
}

1.3 更好的提示词

提示词 P01:修复现有 useEffect

使用时机:你有一个 useEffect,行为异常(数据不更新或无限请求)

帮我检查和修复这个 React 19.2.6 组件的 useEffect。

当前代码:
[粘贴完整组件代码]

问题现象:
[选择一个:
- 数据不更新(修改 X 后,显示的还是旧数据)
- 无限请求循环(Network 面板里请求不停)
- 第一次加载正常,切换参数后不更新
- ESLint 报 react-hooks/exhaustive-deps 警告]

需要:
1. 找出依赖数组的问题(遗漏/多余/每次创建新引用)
2. 判断这个 useEffect 是否真的需要(还是用 useMemo/事件处理更合适)
3. 如果数据获取场景,是否可以改用 React 19 的 use() hook
4. 修复后的代码

基于 React 19.2.6。

提示词 P02:React 19 use() hook 数据获取

使用时机:用 use() 替代 useEffect + useState 组合获取数据

帮我用 React 19.2.6 的 use() hook 重写数据获取逻辑。

当前代码(useEffect 模式):
[粘贴 useEffect + useState 的数据获取代码]

要求:
1. 用 use() hook 替代 useEffect + loading/error state 组合
2. 配合 Suspense 处理 loading 状态
3. 配合 ErrorBoundary 处理错误(React 19 的 function ErrorBoundary 语法)
4. 如果有多个并发数据请求,如何用 use() 并行处理?

注意:
- use() 只能在组件顶层或循环中调用(不能在条件语句里)
- 需要对 Promise 进行缓存(不然每次渲染都创建新 Promise)
- 如何用 React.cache() 或外部缓存稳定 Promise 引用?

给我完整的代码,包括 Suspense 和 ErrorBoundary 的嵌套结构。

基于 React 19.2.6。

提示词 P03:useEffect 架构审查

使用时机:Review 整个组件文件,找出所有 useEffect 滥用

帮我审查这个 React 19.2.6 文件里的所有 useEffect,识别可以简化的模式。

代码文件:
[粘贴整个文件]

审查标准:

1. 依赖数组完整性:
   - 是否有遗漏的依赖?(会导致数据陈旧)
   - 是否有每次渲染都变的依赖?(会导致无限循环)

2. 是否真的需要 useEffect?对每个 effect 判断:
   - 如果是根据 state/props 计算数据 → 改用 useMemo
   - 如果是事件响应 → 改用事件处理函数
   - 如果是数据获取 → 考虑改用 use() hook 或 React Query
   - 如果是第三方库初始化(非React库) → useEffect 合适

3. React 19 可以简化的模式:
   - useEffect + setState 数据获取 → use() hook
   - useEffect + context → use(MyContext) 直接读取
   - useEffect 同步外部状态 → useSyncExternalStore

给我按优先级排列的改进建议,以及每个改进的完整代码。

基于 React 19.2.6。

1.4 验收清单

检查项 验证方法 AI辅助
ESLint 无 exhaustive-deps 警告 npm run lint 无警告 用 P01 修复
eslint-disable 注释 grep 搜索 eslint-disable 用 P01 分析
数据获取不用 useEffect+useState 用 use() 或 React Query 用 P02 改造
对象依赖用 useMemo 稳定 无对象字面量在依赖数组里 用 P01 修复
派生数据用 useMemo 不用 useEffect 无 useEffect → setState 计算模式 用 P03 审查
功能验证:修改参数后数据正确刷新 手动测试切换 userId 等参数 -

1.5 本章小结

如果你只记一件事:遇到 ESLint 的 react-hooks/exhaustive-deps 警告,不要用 // eslint-disable-next-line 忽略。要么把遗漏的依赖加上,要么认真想想这个 useEffect 是否真的必要——很多时候,计算派生数据用 useMemo,响应事件用事件处理函数,都比 useEffect 更好。

useEffect 依赖的三个层次

  1. 正确的依赖数组(完整 + 稳定引用):所有在 effect 里用到的变量都要在依赖里,对象/函数依赖要用 useMemo/useCallback 稳定引用
  2. 识别不需要 useEffect 的场景(计算派生数据用 useMemo,事件响应用处理函数):减少不必要的 effect,降低 Bug 概率
  3. React 19 新方案(use() hook + Suspense):数据获取场景用 use(),彻底消除依赖数组问题,代码更简洁

→ 第2章:AI帮我用了useState但没有处理异步状态