在React开发中,useEffect 是最常用也是最容易踩坑的 Hook 之一。其中最典型的问题就是无限循环(infinite loop),不仅会导致性能问题,严重时甚至让页面直接卡死。本文将从原理、常见错误到实战优化,系统讲清如何避免这一问题。
为什么 useEffect 会出现无限循环?
从本质上讲,useEffect 的执行遵循这样一个循环:
- 组件渲染
- 执行 useEffect
- useEffect 内部修改 state
- 触发重新渲染
- 如果依赖变化 → 再次执行 useEffect
如果这个过程没有终止条件,就会形成死循环。React 官方也明确指出:无限循环通常发生在Effect 修改了状态,而该状态又在依赖数组中。
最常见的 5 种错误写法
1. 没有依赖数组
useEffect(() => {
setCount(count + 1);
});
问题:每次渲染都会执行 → 无限循环
解决方法:
useEffect(() => {
setCount(count + 1);
}, []);
2. 依赖项写错(把 state 自己写进去)
useEffect(() => {
fetchData();
setData(...);
}, [data]); // 错误,把 state 自己写进去
问题:
- data 改变 → 触发 effect
- effect 又 setData → data 再变 → 死循环
解决方法:
useEffect(() => {
fetchData();
}, []);
3. 依赖对象或数组(引用变化)
useEffect(() => {
doSomething();
}, [options]); // options 是对象
问题:对象每次 render 都是新引用 → React 认为“变了” → 无限执行
解决方法:
const options = useMemo(() => ({ a: 1 }), []);
4. 依赖函数(每次重新创建)
useEffect(() => {
fetchData();
}, [fetchData]);
问题:函数每次 render 都是新引用 → 无限循环
解决方法:
const fetchData = useCallback(() => {
...
}, []);
5. 在 Effect 中无条件 setState
useEffect(() => {
setVisible(true);
}, [visible]);
问题:setState → render → effect → setState → 循环
解决方法:增加判断条件
useEffect(() => {
if (!visible) {
setVisible(true);
}
}, [visible]);
实战中最有效的 6 个解决方案
1. 正确使用依赖数组(最重要)
- []:只执行一次(组件挂载)
- [state]:仅在 state 变化时执行
2. 使用 useMemo / useCallback 稳定引用
- 对象 → useMemo
- 函数 → useCallback
这是避免隐形循环的关键手段。
3. 不要用 useEffect 处理纯计算逻辑
如果只是根据 props/state 计算值:
错误:
useEffect(() => {
setTotal(price * count);
}, [price, count]);
正确:
const total = price * count;
这是很多项目性能问题的根源。
4. 避免 Effect 管理业务流程
React 官方建议:如果不是和外部系统(API、DOM、订阅)交互,就不应该用 useEffect。
5. 拆分 useEffect
不要写一个大而全的 effect:
// 错误写法
useEffect(() => {
fetchData();
setTitle();
addListener();
}, [a, b, c]);
拆分:
useEffect(fetchData, []);
useEffect(setTitle, [title]);
useEffect(addListener, []);
6. 使用 ESLint 检查依赖
推荐使用:
eslint-plugin-react-hooks
它可以自动提示依赖问题,避免 80% 的坑。
一个完整正确示例
const App = () => {
const [data, setData] = useState([]);
const fetchData = useCallback(async () => {
const res = await fetch('/api');
const json = await res.json();
setData(json);
}, []);
useEffect(() => {
fetchData();
}, [fetchData]);
return <div>{data.length}</div>;
};
- 不会无限循环
- 依赖清晰
- 性能稳定
总结
避免 useEffect 无限循环的核心原则只有一句话:确保依赖稳定 + 避免在 effect 中无条件触发 state 更新。
可以记住这 3 条黄金法则:
- 依赖必须稳定(useMemo / useCallback)
- state 更新必须有条件
- 非副作用逻辑不要用 useEffect
掌握这些,你基本可以彻底摆脱 useEffect 的循环问题。