在React中使用useEffect
处理副作用时,有很多细节需要注意,否则容易引发性能问题、逻辑错误甚至内存泄漏。结合实际开发经验,整理了这些关键注意事项:
1. 明确依赖项数组的作用
useEffect
的第二个参数(依赖项数组)决定了副作用何时执行:
- 空数组
[]
:仅在组件挂载时执行一次(类似componentDidMount
) - 包含特定值:组件挂载时执行,且当数组中的值发生变化时重新执行
- 不传递第二个参数:每次组件渲染后都会执行(谨慎使用)
常见错误:依赖项缺失
// 错误示例:useEffect使用了count却未添加到依赖项
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
console.log(count); // 始终打印0,因为闭包捕获了初始count
}, 1000);
return () => clearInterval(timer);
}, []); // 缺少count依赖
// 正确示例:添加依赖项
useEffect(() => {
const timer = setInterval(() => {
console.log(count);
}, 1000);
return () => clearInterval(timer);
}, [count]); // 依赖count,变化时重新创建定时器
2. 清理函数必须正确处理副作用
useEffect
的返回函数(清理函数)用于清除副作用,避免内存泄漏:
- 清理订阅(事件监听、WebSocket等)
- 取消网络请求
- 清除定时器/计时器
反例:未清理事件监听导致多次绑定
// 错误示例:组件更新时会重复绑定事件
useEffect(() => {
window.addEventListener('resize', handleResize);
// 缺少清理函数
}, []);
// 正确示例:添加清理函数
useEffect(() => {
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize); // 移除监听
};
}, [handleResize]);
3. 避免在effect中修改依赖项
在useEffect
中修改依赖项会导致无限循环:
// 错误示例:修改依赖项count导致无限渲染
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // 修改了依赖项count
}, [count]); // 依赖count变化,触发effect再次执行
// 正确示例:使用函数式更新避免依赖
useEffect(() => {
setCount(prev => prev + 1); // 不依赖外部count变量
}, []); // 空依赖,仅执行一次
4. 处理异步操作的正确姿势
在useEffect
中使用异步函数时,需注意:
- 不能直接将effect函数定义为async(因为它需要返回清理函数)
- 异步操作取消时需处理状态更新,避免内存泄漏
// 正确写法:在effect内部定义async函数
useEffect(() => {
let isMounted = true; // 标记组件是否已挂载
const fetchData = async () => {
try {
const res = await fetch('/api/data');
const data = await res.json();
if (isMounted) { // 仅在组件挂载时更新状态
setData(data);
}
} catch (err) {
console.error(err);
}
};
fetchData();
return () => {
isMounted = false; // 组件卸载时标记为false
};
}, []);
5. 性能优化:避免不必要的effect执行
- 合并相关effect:多个不相关的副作用应拆分为多个
useEffect
- 使用
useCallback
/useMemo
处理依赖:避免因函数/对象引用变化导致effect频繁执行
// 优化前:handleChange每次渲染都会创建新引用,导致effect频繁执行
useEffect(() => {
window.addEventListener('scroll', handleChange);
return () => window.removeEventListener('scroll', handleChange);
}, [handleChange]); // handleChange变化触发effect
// 优化后:使用useCallback缓存函数引用
const handleChange = useCallback(() => {
// 处理滚动逻辑
}, []); // 空依赖,函数引用稳定
useEffect(() => {
window.addEventListener('scroll', handleChange);
return () => window.removeEventListener('scroll', handleChange);
}, [handleChange]); // 仅在handleChange真正变化时执行
6. 注意闭包陷阱
useEffect
中的函数会捕获当前渲染周期的状态和 props,不会自动获取最新值:
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
console.log('当前count:', count); // 始终打印effect执行时的count值
}, 1000);
return () => clearInterval(timer);
}, []); // 空依赖,count始终是初始值0
// 解决方案:将依赖项加入数组,或使用ref保存最新值
7. 服务端渲染的特殊处理
在Next.js等框架中,useEffect
在服务端不会执行,仅在客户端执行:
- 避免在effect中处理服务端必须完成的逻辑
- 如需在客户端初始化时执行,可配合
useEffect
+空依赖实现
总结
useEffect
的核心原则是:明确副作用的依赖范围,及时清理副作用,避免不必要的执行。使用时可借助ESLint的react-hooks/exhaustive-deps
规则检测依赖项问题,减少手动失误。记住,每个useEffect
应只负责处理单一职责的副作用,保持代码清晰可维护。