严格模式 useEffect 执行两次:React 18 的刻意为之
升级 React 18 后,测试环境的日志全量翻倍,每个请求都发了两遍。新同事以为是 bug 要修,其实这是 StrictMode 在开发模式的刻意行为:组件挂载 → 立即卸载 → 再次挂载,effect 自然跑两次。生产构建不会发生。
官方做这个设计是为了逼出「不纯的 effect」。React 未来版本的可中断渲染要求 effect 具备可重复执行的安全性,mounted 里建立的订阅、定时器如果不清理,双调用立刻暴露问题——正好第二遍挂载时上一遍的资源还挂着,控制台里看得清清楚楚。
看一个典型的暴露现场:
useEffect(() => {
const source = new EventSource('/api/stream') // 建立了就不管了
source.onmessage = (e) => setData(JSON.parse(e.data))
}, []) // 没有清理函数
严格模式下这个 effect 跑两次,两个 EventSource 同时存在,连接数翻倍,服务器日志一目了然。修复方式是补全生命周期:
useEffect(() => {
const source = new EventSource('/api/stream')
source.onmessage = (e) => setData(JSON.parse(e.data))
return () => source.close() // 卸载时关掉,双调用也安全
}, [])
带清理函数之后,两次执行的序列变成「建、拆、建」,结束时恰好剩一个连接,幂等了。
面对双执行有三种态度。第一种是关掉 StrictMode,图省事但放弃了所有检查,不推荐。第二种是用一个标志位防止 effect 跑两次,比如模块级的 didInit 变量——这其实是自欺欺人,生产环境的 remount(路由切换回来)依然会出问题。第三种是正面修:所有 effect 都配对清理逻辑,请求用 AbortController,监听器成对 add/remove,定时器 set/clear。
请求发两遍的真实处理示例:
useEffect(() => {
const ctrl = new AbortController()
fetch('/api/user', { signal: ctrl.signal })
.then((r) => r.json())
.then(setUser)
.catch((e) => {
if (e.name !== 'AbortError') throw e // 被中止的请求不算错误
})
return () => ctrl.abort() // 卸载时取消在途请求
}, [])
我们的落地约定:StrictMode 只在开发与测试入口开启(它本身就建议不进生产包),所有「开启型」副作用必须有 cleanup。跑了两周,揪出了三处事件监听泄漏和一处定时器未清理,这个「烦人」的特性确实在干活。
- 点赞
- 收藏
- 关注作者
评论(0)