最近在做项目的时候,我遇到了一个极其让人抓狂的 Bug:页面只要稍微操作一下无关的状态,网络请求就会疯狂发送,直接把浏览器卡死。
在排查这个 Bug 的过程中,我重新审视了 React 中 useEffect 的依赖机制,以及被无数人奉为性能优化神器的 useMemo。今天就来复盘一下,希望能帮大家避开这个“初学者必踩”的大坑。
一、现场代码:问题其实很隐蔽
业务很简单:父组件维护一个计数器,子组件根据查询条件发请求。
1 | import { useEffect, useState } from 'react'; |
表面上看,config 的内容根本没变。可只要 count 一更新,DataList 里的 useEffect 就会重新执行。
这里先说严谨一点:这段代码本身会导致的是“effect 重跑”,不一定自动变成“无限请求”。
真正把页面拖到卡死的,往往是 effect 里还夹着别的副作用,比如:
- 请求成功后又触发了某个
setState - 触发了上报、轮询或重试逻辑
- 父子组件之间还有别的联动更新
这些东西叠在一起,才会放大成请求风暴。
可以把这个过程理解成下面这张图:
引用不同
点击按钮,count 更新
App 重新执行
重新创建 config
新引用传给 DataList
依赖比较
依赖已变化
effect 重跑,请求再次触发
二、根因不复杂:变的不是内容,是引用
React 函数组件每次重新渲染,函数体都会重新执行。
所以这行代码:
1 | const config = { status: 'active', keyword: 'react' }; |
会在每次渲染时重新创建一个新对象。
虽然它看起来和上一次一模一样,但对 JavaScript 来说,这是一个新的引用。
而 useEffect 的依赖数组会逐项用 Object.is() 比较。对对象这种引用类型来说,比较的不是字段内容是否相同,而是“是不是同一个对象”。
于是 React 会得到这样的结论:
- 上一次的
queryConfig是对象 A - 这一次的
queryConfig是对象 B - A !== B
- 所以依赖变了,
effect重新执行
问题到这里,其实已经讲完了。它和“React 神秘机制”没关系,就是一次很典型的引用稳定性问题。
三、第一反应用 useMemo,不算错,但也别上头
很多人看到这里,第一反应就是:那我把对象 memo 一下不就解决了。
1 | const config = useMemo(() => { |
这个写法能不能解决问题?能。
但它是不是这个场景下最好的写法?未必。
原因不在于 useMemo “性能特别差”,这话说重了。大多数场景里,useMemo 那点开销根本不是瓶颈。真正的问题是:你为一个本来可以写得更直接的静态值,引入了额外的心智负担。
你需要多维护一层 Hook;你需要保证依赖数组永远正确;你还把“这是个静态配置”这个事实藏了起来。
换句话说,在这个例子里,useMemo 不是错,而是绕。
四、这个场景更直接的解法:把静态常量移出组件
如果一个值根本不依赖组件内的 state 和 props,那它就不该跟着组件一起重新创建。
最简单的写法其实是这样:
1 | import { useEffect, useState } from 'react'; |
这样改的好处很直接:
- 引用天然稳定
- 不需要额外 Hook
- 代码意图更清楚
- 后续维护时不容易漏依赖
所以这里更准确的经验不是“别用 useMemo”,而是:能外置的静态值,优先外置。
改成组件外常量后,流程就会简单很多:
只创建一次
引用相同
组件外常量
DEFAULT_CONFIG
点击按钮,count 更新
App 重新执行
DataList 收到同一个引用
依赖比较
依赖未变化
effect 不重跑
五、那什么时候真的该用 useMemo?
真正适合 useMemo 的,通常是下面两类场景。
第一类:计算结果确实昂贵。
比如你要基于一个大数组做排序、过滤、分组,而且输入没变时你不想重复算。
1 | const visibleList = useMemo(() => { |
第二类:你确实需要一个稳定引用,但这个值又不能移到组件外。
比如它依赖当前组件的 state/props,并且:
- 要传给
React.memo包裹的子组件 - 要作为另一个 Hook 的依赖
- 你已经确认频繁重建它会带来实际问题
这种情况下,useMemo 是合理的:
1 | const config = useMemo(() => { |
注意这里和前面的区别:这个 config 真的依赖动态值,所以它不能直接外置。此时 useMemo 才是“稳定引用”的正经方案。
六、顺手纠正一个常见误区
很多文章喜欢把 useMemo 讲成“性能优化神器”,也有一些文章喜欢反过来把它打成“负优化元凶”。这两种说法都太满。
更稳的理解是:
useMemo是一个工具,不是默认选项- 它主要解决“重复计算”或“引用稳定性”问题
- 用不用,取决于值的来源、使用位置和真实收益
如果一个值是静态常量,外置通常比 useMemo 更干净。
如果一个值依赖运行时数据,而且你确实需要稳定它,useMemo 就很合适。
七、总结成三条判断规则
- 如果值不依赖组件内状态,优先移到组件外。
- 如果值依赖运行时数据,但创建成本低、也不会影响子组件或 Hook,就别急着
memo。 - 只有在“重复计算昂贵”或“稳定引用确实有收益”时,再用
useMemo。
让我们回看这次问题,真正该记住的并不是“useMemo 好不好用”,而是另一句话:
先判断值应该存在于哪里,再决定要不要缓存它。
这比一上来就套 Hook,更接近工程上的最佳实践。
最后,如果你有什么好的想法或建议,欢迎在评论区留言,与我一起探讨。