大家好,我是此林。
今天来记录下 useEffect Hook 的学习笔记。
useEffect 干嘛的?
useEffect 是 React Hooks 里用来处理副作用的。
作用:
组件渲染完之后,要做点"额外的事",就会用到 useEffect。
说到浏览器渲染,就不得不说下 React 的完整渲染周期和 Hook 执行时机了。

可以看到,useEffect 在浏览器绘制之后才执行。
什么叫"副作用"?
不是"坏事",而是不直接参与渲染 UI 的操作,比如:
-
请求接口(fetch / axios)
-
操作 DOM
-
监听事件(scroll、resize)
-
定时器(setInterval / setTimeout)
-
本地存储(localStorage)
-
订阅 / 取消订阅
基本用法
useEffect(() => {
// 副作用代码
}, [依赖]);
1. 每次渲染后都执行
useEffect(() => {
console.log('组件渲染了');
});
不写依赖数组:
-
初次渲染执行
-
每次 state / props 改变都会执行
这个因为再每次 state 状态改变后都会重新执行,通常不是我们想要的,可能会引发 bug 和性能浪费,写代码的时候要谨慎。
2. 只在组件挂载时执行(最常见)
useEffect(() => {
fetchData();
}, []);
依赖数组是空 []
等价于 class 组件的:
componentDidMount()
常用于:
-
请求接口(组件挂载后初始化获取API数据)
-
初始化逻辑
3. 依赖某个 state / props 改变时执行
useEffect(() => {
console.log('count 变了', count);
}, [count]);
只有 count 改变时才执行
等价于:
componentDidUpdate(prevProps, prevState)
4. 清理副作用(非常重要)
useEffect(() => {
const timer = setInterval(() => {
console.log('running');
}, 1000);
return () => {
clearInterval(timer);
};
}, []);
可以看到这是个定时器,首先
useEffect(..., [])
[]表示 只在组件第一次渲染(挂载)时执行不会因为 state 更新再执行
setInterval
const timer = setInterval(() => { console.log('running'); }, 1000);每 1 秒 执行一次
控制台一直打印
running
return () => {
clearInterval(timer);
};
这是 清理函数(cleanup):会组件卸载时执行
或 effect 重新执行前执行(这里不会重新执行)
作用:防止定时器泄漏
return 的函数等价于:
componentWillUnmount()
对于 React 的 Class API,虽然 class 组件从来没有被正式废弃(deprecated) ,
但 从 React 16.8(2019)开始已经不再是推荐写法。
官方态度是:
新功能只围绕 Hooks 设计
文档默认全部用函数组件
class API 只维护,不增强
useEffect 原理
之前说到 useEffect 有依赖项,那 React 是如何判断依赖项发生了变化呢?

不过要注意一点,因为React使用的是浅比较,如果依赖项传入的是对象,那么React只会比较内存引用,而不会比较内部字段属性。
常见坑
一、依赖项漏写或写错
坑 1:忘记写依赖数组
useEffect(() => {
fetchData()
})
问题
-
每次 render 都会执行
-
造成 死循环(特别是 effect 里 setState)
正确
useEffect(() => {
fetchData()
}, [])
[] 表示 只在组件挂载时执行一次
坑 2:依赖项漏写(最常见)
useEffect(() => {
console.log(count)
}, [])
问题
count更新了,但 effect 用的还是 旧值(闭包陷阱)
正确
useEffect(() => {
console.log(count)
}, [count])
原则:effect 中用到的外部变量,都应该出现在依赖数组中
二、闭包陷阱
闭包问题,在定时器/异步场景下出现的比较多,需要特别注意。
坑 3:setInterval / setTimeout 用了旧状态
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1)
}, 1000)
return () => clearInterval(timer)
}, [])
问题
count永远是初始值
方案 1:函数式更新(推荐)
setCount(c => c + 1)
方案 2:把依赖加进去(不适合 interval)
}, [count])
这些场景都有一个共同点:
useEffect([])只创建一次回调函数 不会随 render 更新
回调只能看到 第一次 render 的变量
典型场景包括:
setInterval
setTimeout
addEventListenerWebSocket 回调
Promise.then
只要回调"活得比一次 render 久",就要小心闭包
三、无限循环问题
坑 4:依赖里放了 effect 内部修改的 state
useEffect(() => {
setCount(count + 1)
}, [count])
结果
- render → effect → setState → render → ... 无限循环
正确
-
明确「什么时候应该触发 effect」
-
或用条件限制
useEffect(() => {
if (count < 10) {
setCount(c => c + 1)
}
}, [count])
四、对象 / 数组作为依赖
坑 5:依赖对象 / 数组导致频繁触发
useEffect(() => {
doSomething()
}, [{ a: 1 }])
问题
-
每次 render 都是新对象
-
effect 每次都执行
正确(使用 useMemo 缓存对象)
const obj = useMemo(() => ({ a: 1 }), [])
useEffect(() => {
doSomething()
}, [obj])
或直接拆成基础类型:
useEffect(() => {
doSomething()
}, [a])
还有一种方法,就是移到函数组件外部,此时他作为一个常量,永远不会变。
关于这个点,我们之前在 useEffect 原理已经说过了,React 底层使用的是浅比较。
五、函数作为依赖
坑 6:函数每次 render 都变
useEffect(() => {
handler()
}, [handler])
问题
- 普通函数在 render 时会重新创建
正确
const handler = useCallback(() => {
...
}, [])
useEffect(() => {
handler()
}, [handler])
useCallback 和 useMemo 很像,只不过 useMemo 缓存的值,useCallback 缓存的是函数本身。
六、清理副作用没写
坑 7:忘记 cleanup
useEffect(() => {
window.addEventListener('resize', onResize)
}, [])
问题
-
内存泄漏
-
事件重复绑定
正确
useEffect(() => {
window.addEventListener('resize', onResize)
return () => {
window.removeEventListener('resize', onResize)
}
}, [])
七、useEffect ≠ 生命周期完全等价
坑 8:把所有逻辑都塞进 useEffect
useEffect(() => {
setState(deriveFromProps(props))
}, [props])
问题
-
不必要的 render
-
可读性差
正确思路
-
能在 render 期间计算的,不要放 effect
const derived = useMemo(() => {
return deriveFromProps(props)
}, [props])
八、React 18 + StrictMode 特殊坑
坑 9:开发环境 effect 执行两次
useEffect 执行了 2 次?
原因
-
React 18 StrictMode 故意 double invoke
-
只在开发环境
解决
-
不要依赖「只执行一次的副作用逻辑」
-
所有 effect 都应是 可安全重复执行的
总结 useEffect 三问:
-
什么时候执行?(依赖是谁)
-
用了哪些外部变量?(依赖全写)
-
有没有副作用需要清理?(return)

之前我们说了 React 渲染流程,再看看这张图。

注意这里 useEffect 是在浏览器绘制后异步执行的,这样做有什么好处呢?
不阻塞浏览器绘制,用户响应更快,适合大多数副作用场景(数据获取API调用、事件监听器、订阅管理、日志记录等)
不过 React 里还有个类似的 Hook,它叫 useLayoutEffect。
useLayoutEffect?

由上表可知,useLayoutEffect 在浏览器绘制之前同步执行,可能会导致卡顿,我们用的比较少。
绝大多数副作用场景优先使用 useEffect,仅在有视觉问题等特殊场景才使用 useLayoutEffect。
经典的闪烁问题
下面来理解下 useLayoutEffect 存在的意义。

如果我们使用 useEffect,因为它是在浏览器绘制后才异步执行的,
那么 React 会先渲染一个宽度为 0 的 div,浏览器把宽度为 0 的 div 绘制到屏幕上;
然后 useEffect 才执行把宽度改为 100px,浏览器重新绘制。
这就有可能导致用户肉眼看到 div 从 0 闪到 100px 的过程,体验感差。
所以我们使用 useLayoutEffect 解决这个问题,
首先 React 依然会先渲染一个宽度为 0 的 div,但是在交给浏览器绘制之前,useLayoutEffect 会同步地立刻把 div 宽度改为 100px,从而避免了视觉闪烁问题。