人人都会AI编程

useEffect 滥用与无限循环

更新时间:2026-07-11

useEffect 是 React 中处理副作用的万能工具,但也是开发者最容易踩坑的 API 之一。理解它的执行时机和依赖项机制,才能避免渲染死循环和逻辑异常。

依赖项缺失导致的死循环

最常见的问题是在 useEffect 内部修改了状态,却没有将该状态放入依赖数组,或者依赖项设置不当,导致组件无限重渲染。

错误示例一:更新状态却没有声明依赖

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    // 每次渲染后都会执行,内部又调用 setCount
    setCount(count + 1);
  }); // 没有依赖数组,每次重渲染都触发 effect,effect 里又更新状态 → 死循环
}

修复:明确 effect 的触发条件。如果想在组件挂载时执行一次,传入空数组 [];或者声明正确的依赖项。

useEffect(() => {
  setCount(c => c + 1); // 使用函数式更新,无需依赖 count
}, []); // 只在挂载时执行一次

错误示例二:对象或数组作为依赖项,引用每次变化

function Profile({ user }) {
  const [friends, setFriends] = useState([]);

  useEffect(() => {
    fetchFriends(user.id).then(setFriends);
  }, [user]); // user 是对象,哪怕内容没变,每次渲染也会产生新引用?
}

如果父组件这样传递 user

<Profile user={{ id: 1, name: 'Alice' }} />

即使值相同,每次渲染都会创建一个新的对象字面量,导致 user 引用变化,useEffect 反复执行,可能触发无限请求。

修复:用 useMemo 稳定引用,或只在依赖项中使用原始值。

// 父组件中
const user = useMemo(() => ({ id: 1, name: 'Alice' }), []);
// 或直接传 primitive
<Profile userId={1} />

错误示例三:effect 内部修改了依赖项本身

const [list, setList] = useState([1, 2, 3]);

useEffect(() => {
  setList([...list, 4]);
}, [list]); // setList 后 list 变化,再次触发 effect → 无限循环

修复:使用函数式更新,不依赖 list

useEffect(() => {
  setList(prev => [...prev, 4]);
}, []); // 空数组仅在挂载时执行

滥用 useEffect 处理派生状态

有时候开发者习惯于把所有逻辑扔进 useEffect,包括那些可以通过纯计算得到的状态(派生状态)。这会导致多余的渲染和循环风险。

反模式:用 useEffect 同步派生状态

const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');

useEffect(() => {
  setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);

每次 firstNamelastName 变化,会触发两次渲染:第一次是状态本身变化,第二次是 effect 更新 fullName。这不仅浪费性能,还容易在复杂逻辑中引入不一致。

正确做法:直接计算

const fullName = firstName + ' ' + lastName; // 渲染期间直接计算

只有在需要异步操作、订阅、DOM 操作等真实副作用时,才使用 useEffect

忘记清理副作用

如果 effect 中创建了定时器、订阅或事件监听,但没有提供清理函数,会导致内存泄漏,也可能意外触发多次 effect 执行时出现竞态。

示例:定时器未清理

useEffect(() => {
  const id = setInterval(() => {
    console.log('tick');
  }, 1000);
  // 缺少清理函数,组件卸载后定时器依然运行
});

修复:

useEffect(() => {
  const id = setInterval(() => {
    console.log('tick');
  }, 1000);
  return () => clearInterval(id);
}, []);

竞态条件(Race Condition)

当 effect 内部发起异步请求,且依赖项频繁变化时,有可能后发出的请求先返回,导致过期数据覆盖最新状态。

useEffect(() => {
  fetch(`/api/user/${id}`)
    .then(res => res.json())
    .then(data => setUser(data));
}, [id]);

如果快速切换 id,可能旧请求的数据稍后到达,最终显示错误的用户信息。

修复:使用清理函数忽略过期的 effect

useEffect(() => {
  let cancelled = false;
  fetch(`/api/user/${id}`)
    .then(res => res.json())
    .then(data => {
      if (!cancelled) setUser(data);
    });
  return () => { cancelled = true; };
}, [id]);

如何识别与避免无限循环

  1. 打开 React DevTools 的 “Highlight updates”:观察哪些组件频繁重渲染。
  2. 在 effect 中添加日志:打印依赖项值,观察是否连续变化。
  3. 检查依赖项是否使用了对象、数组或函数:考虑使用 useMemouseCallback 或提取原始值。
  4. 优先使用函数式 setState:避免将 state 写入依赖数组。
  5. 善用 ESLint 插件 eslint-plugin-react-hooks:它会提示遗漏的依赖项,但也需理性对待,不要盲目添加所有依赖。

遵循一个简单原则:如果一段代码不需要异步或 DOM 操作,就不应该放进 useEffect。将副作用真正限制在“同步外部系统”的范围内,React 应用的稳定性和可预测性将大幅提升。