严格模式 useEffect 执行两次:React 18 的刻意为之

举报
yd_264617019 发表于 2026/10/17 16:51:52 2026/10/17
【摘要】 升级React18后,测试环境的日志全量翻倍,每个请求都发了两遍。新同事以为是bug要修,其实这是StrictMode在开发模式的刻意行为:组件挂载→立即卸载→再次挂载,effect自然跑两次。生产构建不会发生。官方做这个设计是为了逼出「不纯的effect」。React未来版本的可中断渲染要求effect具备可重复执行的安全性,mounted里建立的订阅、定时器如果不清理,双调用立刻暴露问题...

升级 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。跑了两周,揪出了三处事件监听泄漏和一处定时器未清理,这个「烦人」的特性确实在干活。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。