React StrictMode 为什么会让某些逻辑执行两次?
简化版
React StrictMode 在开发模式下会故意重复调用某些渲染和副作用流程,用来暴露不纯渲染、未清理订阅、重复请求等问题。它不影响生产环境,但提醒你副作用必须可清理、渲染必须保持纯净。
详细版
React 18 开发模式中,StrictMode 可能让组件经历挂载、清理、再挂载的检查流程。
useEffect(() => {
console.log('subscribe')
return () => console.log('cleanup')
}, [])
你可能看到 subscribe -> cleanup -> subscribe。这不是 React 生产环境 bug,而是开发期检查。正确做法不是关掉 StrictMode 掩盖问题,而是保证 effect 有清理逻辑、请求有取消或幂等处理、渲染阶段不做副作用。
完整版教学
一、StrictMode 是开发期体检器
StrictMode 不会渲染任何真实 DOM。 它包住组件树后,会启用一组开发模式检查,帮助你提前发现未来并发渲染和重复挂载下的隐患。 最容易被注意到的就是某些逻辑看起来执行了两次。
<React.StrictMode>
<App />
</React.StrictMode>
如果代码只在“永远只执行一次”的假设下正确,遇到重试、恢复、并发中断就可能出问题。 StrictMode 用更严格的开发体验逼你写出可重复、可清理的代码。
二、渲染阶段必须是纯计算
React 组件函数本身属于渲染阶段。 渲染阶段应该只根据 props 和 state 返回 UI,不应该发请求、改全局变量、订阅事件或写 DOM。 如果渲染函数被调用两次就出错,说明它不纯。
function BadComponent() {
analytics.track('render') // 不推荐:渲染阶段副作用
return <div>hello</div>
}
| 位置 | 是否适合副作用 | 原因 |
|---|---|---|
| 组件函数体 | 不适合 | 可能被重复调用或丢弃 |
useMemo 计算函数 | 不适合副作用 | 仍应保持纯计算 |
useEffect | 适合异步副作用 | 可清理 |
| 事件处理函数 | 适合用户触发的副作用 | 来源明确 |
比如组件函数里给全局计数器加 1,双调用后数字会变成 2。 这不是 React 错,而是代码把副作用放错位置。
三、effect 要能挂载、清理、再挂载
开发模式下 StrictMode 会模拟组件重新挂载,检查 effect 清理是否完整。 如果你订阅事件但不移除,重复挂载会造成多个监听器。 真实项目里,路由切换或条件渲染也会触发同样问题。
useEffect(() => {
const handler = () => console.log('resize')
window.addEventListener('resize', handler)
return () => window.removeEventListener('resize', handler)
}, [])
开发检查:
mount -> effect
unmount -> cleanup
mount -> effect
如果少了 cleanup,窗口 resize 一次可能打印 2 次、3 次,随着挂载次数增加越来越多。 StrictMode 让这个问题更早暴露。
四、重复请求要靠取消、缓存或幂等处理
很多人第一次遇到 StrictMode 双执行,是发现接口请求发了两次。 真实解决思路不是简单删除 StrictMode,而是让请求逻辑可控。 例如使用 AbortController 取消旧请求,或交给 React Query/SWR 做缓存和去重。
useEffect(() => {
const controller = new AbortController()
fetch('/api/user', { signal: controller.signal })
return () => controller.abort()
}, [])
假设一次请求 120ms,开发模式两次请求可能让你误以为性能翻倍下降。 但生产环境不会因为 StrictMode 额外双发。 真正要关注的是请求是否能取消、是否能抵抗重复提交、接口是否幂等。
五、它和 React 18 并发能力有关
并发渲染里,React 可能开始渲染一棵树,然后因为更高优先级更新中断它。 也可能为了恢复 UI、重试渲染而重复执行某些流程。 如果代码依赖“渲染只发生一次”,未来就不安全。
并发思路:
开始渲染 A
高优先级输入来了
暂停或丢弃 A
先处理输入
之后重新渲染 A
StrictMode 让开发者在普通本地开发中也能感受到这些约束。 它不是为了折磨人,虽然有时确实像在轻轻拍你脑门。
六、生产环境不会做这类开发检查
StrictMode 的重复调用主要发生在开发模式。 生产构建不会因为它额外执行这些检查。 但不要因此忽略问题,因为开发期暴露的副作用不纯,在真实卸载重挂、网络重试、并发中断中仍可能造成 bug。
记忆钩子:StrictMode 不是让生产跑两遍,而是在开发时问你“代码重来一次还稳不稳”。
面试回答时要避免只说“开发环境会执行两次”。 更重要的是说清楚为什么:检查渲染纯度和副作用清理能力。
七、常见误区与追问
- 误区:StrictMode 导致生产环境接口也发两次。 这类重复检查发生在开发模式,生产构建不会因此双执行。
- 误区:解决方式就是删掉 StrictMode。 更应该修复不纯渲染、缺失清理和重复副作用。
- 误区:空依赖 useEffect 永远只执行一次。 在开发 StrictMode 检查中,可能经历挂载、清理、再挂载。
- 追问:渲染阶段为什么不能发请求? 渲染可能被重复、暂停或丢弃,副作用应放到 effect 或事件中。
- 追问:重复请求怎么处理? 使用取消、缓存去重、请求幂等或成熟数据请求库。
- 追问:StrictMode 会影响页面 DOM 吗? 它本身不额外渲染 DOM 节点,只启用开发检查。
八、加强记忆
StrictMode 这题抓“开发检查”和“副作用纪律”。组件渲染要纯,effect 要清理,请求要能取消或去重。看到执行两次先别慌,这是 React 在开发模式提前帮你抓隐藏问题。