在 React 开发中,刚入门的新手都会遇到这样一个问题:
“为什么我只写了一次网络请求代码,结果却反复执行?”
这个问题背后其实隐藏着一个非常常见的误区:把异步请求直接写在组件函数里。 这会导致页面每次重新渲染时都重复发起请求,不仅浪费资源,还可能引发 bug。
今天我们就来详细讲解这个错误,并教你如何用 useEffect 正确地发起一次请求。
场景模拟:一个简单的计数器组件
我们来看一个例子:
import { useState } from "react";
function App() {
const [num, setNum] = useState(0);
// 模拟异步请求
fetchData().then(data => {
setNum(data);
});
return (
<div onClick={() => setNum(num + 1)}>
当前数值:{num}
</div>
);
}
// 模拟网络请求
function fetchData() {
return new Promise(resolve => {
setTimeout(() => resolve(666), 1000);
});
}
export default App;
看起来是不是很熟悉?这段代码的功能也很简单: 页面显示一个数字; 初始值是 0; 然后从服务器获取数据(模拟返回 666); 点击页面可以让数字加 1。 但你有没有发现一个问题?
错误行为:每次渲染都在请求!
第一步:页面加载 → 发起请求; 第二步:数据返回 → 设置 num = 666 → 组件重新渲染; 第三步:重新渲染 → 又执行一遍 fetchData() → 再次发起请求; 第四步:再次点击页面 → 更新状态 → 组件又重新渲染 → 又发起一次请求…… 这样就形成了一个“无限循环”的请求过程!
为什么会这样?
因为 React 的函数组件本质上是一个函数,它会在每次状态更新后重新执行。
所以如果你把异步请求直接写在组件函数里:
fetchData().then(...);
那每次组件重新渲染时,都会重新执行这一行代码,从而导致请求被反复调用。
正确做法:使用 useEffect 控制副作用执行时机
React 提供了一个专门处理副作用的 Hook —— useEffect。
我们可以这样改写上面的代码:
import { useState, useEffect } from "react";
function App() {
const [num, setNum] = useState(0);
useEffect(() => {
fetchData().then(data => {
setNum(data);
});
}, []); // 注意这里有个空数组 []
return (
<div onClick={() => setNum(num + 1)}>
当前数值:{num}
</div>
);
}
function fetchData() {
return new Promise(resolve => {
setTimeout(() => resolve(666), 1000);
});
}
export default App;
这样就能保证:
- 只在组件第一次加载时请求一次
- 后续更新不会再重复请求,避免无限循环
为什么要加上 []?
[]是useEffect的依赖项数组;- 如果是空数组,表示这个副作用只在组件第一次加载时执行一次;
- 后续不管怎么点击、更新状态,都不会再执行这个副作用。 这样一来,就避免了重复请求的问题。
小贴士:什么是副作用?
在 React 中,“副作用”指的是那些不在渲染过程中直接完成的操作,例如:
- 发起网络请求
- 添加事件监听器
- 操作 DOM
- 设置定时器
这些操作会影响外部世界或产生延迟效果,不适合在渲染过程中随意执行。
React 提供了 useEffect 来专门管理这些副作用,让我们可以精确控制它们的执行时机和清理方式。
对比效果:两种写法的区别
不使用 useEffect | 使用 useEffect([], ...) |
|---|---|
| 每次组件重新渲染都会执行请求 | 只在组件首次加载时执行一次 |
| 数据返回后触发更新,再次执行请求 | 数据返回后触发更新,不再重复请求 |
| 导致请求无限循环 | 请求只执行一次,更合理高效 |