React 中的副作用(Side Effects)是指组件渲染之外需要执行的操作,例如数据请求、订阅、定时器、手动修改 DOM 等。useEffect 是处理副作用的主要钩子,但使用不当会引发难以排查的 bug。掌握两个核心原则——依赖项完整和清理函数规范,能帮助你写出健壮且可预测的代码。
依赖项完整:让副作用与状态保持同步
useEffect 的第二个参数是一个依赖数组,React 会在每次渲染后比较数组中的值是否变化,只有变化时才重新执行副作用。依赖项必须包含副作用中使用到的、且会随时间变化的外部变量(props、state、context 等)。遗漏依赖项会导致副作用读取到过期的闭包值,产生不可预期的结果。
❌ 错误示例:遗漏依赖
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser);
}, []); // 空数组表示只在挂载时执行一次
// ...
}
当 userId 变化时,副作用不会重新执行,导致展示的一直是首次传入的用户信息。正确的做法是将 userId 加入依赖:
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser);
}, [userId]);
实用建议:
- 启用 ESLint 的
react-hooks/exhaustive-deps规则,它会自动检测缺失的依赖,这是最可靠的安全网。 - 不要刻意回避依赖:如果你需要执行一个只希望在挂载时运行的副作用(如初始化全局事件),而依赖项会导致重复执行,那通常说明你的逻辑需要重构——或使用
useRef存储不变值,或将副作用拆分成多个useEffect。 - 函数依赖:如果副作用中调用了组件内定义的函数,直接写函数名会导致每次渲染都重新执行。此时可用
useCallback包裹函数,确保引用稳定,或将该函数移入useEffect内部。
正确使用 useCallback 避免不必要的重复执行:
const fetchUser = useCallback((id) => {
return fetch(`/api/users/${id}`).then(res => res.json());
}, []);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId, fetchUser]);
清理函数规范:避免内存泄漏与竞态条件
useEffect 可以返回一个清理函数,React 会在组件卸载或副作用重新执行前调用它。清理函数的核心作用是撤销上一次副作用的影响,常见场景包括:
- 清除定时器
- 取消订阅
- 取消未完成的异步请求(或忽略其回调)
- 移除手动添加的事件监听
❌ 常见反模式:忽略清理导致内存泄漏
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1);
}, 1000);
// 没有返回清理函数,组件卸载后定时器仍在运行
}, []);
✅ 添加清理:
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
处理异步请求的竞态条件:
当依赖项快速变化时,多个异步请求可能并发返回,旧请求的结果可能会覆盖新请求。清理函数可以通过一个标志变量或 AbortController 来取消过期请求:
useEffect(() => {
let cancelled = false;
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
if (!cancelled) setUser(data);
});
return () => {
cancelled = true;
};
}, [userId]);
事件订阅清理:
useEffect(() => {
const handleResize = () => setWindowWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
实战:组织 useEffect 时的思维检查清单
- 问自己:这个副作用依赖哪些外部变量?
把用到的所有 props、state、context 值都列进依赖数组。
- 问自己:副作用是否产生了长期存在的资源?
比如定时器、订阅、WebSocket 连接、手动 DOM 事件——必须返回清理函数。
- 问自己:依赖变化时是否需要先清理再重新执行?
例如根据 userId 获取用户数据,切换用户时应当取消上一次的请求或丢弃其结果。
- 避免在
useEffect中直接使用未在依赖中声明的异步函数:将异步逻辑包装在内部定义函数,确保依赖明确。
- 多个不相关的副作用应该拆分成独立的
useEffect,而不是堆在一个大的钩子里。每个useEffect管理自己的依赖和清理,逻辑更清晰。
遵循这两个原则能让你规避 90% 以上的副作用相关问题,也是面试中高频考察的考点。记住:副作用是必要的“脏活”,但 React 的模型要求你以可预测和可清理的方式去做。