useMemo的理解与应用
只要组件因为别的 state 改了(主题、输入框、弹窗),params 就是新对象 → effect 又跑 → 又请求。第2次渲染(theme 变了,但 chainId/address 没变):useMemo 返回旧的 0xAAA。:你每次渲染都“重新打印一张身份证”,身份证号码当然变 → 系统认为换人了 → effect 触发。“咦,引用从 0xAAA 变成 0xBBB 了 → 依赖变了 → e
useMemo 你就把它当成:“缓存一个计算结果/缓存一个引用”,只有依赖变了才重新算。它解决两类最常见问题:性能 和 引用变化导致的重复触发。
1) useMemo 到底做了什么?
const value = useMemo(() => computeExpensive(x), [x])
-
React 先算一次 computeExpensive(x),把结果 记住
-
之后组件每次重新渲染:
-
如果 x 没变:直接用旧结果,不重新算
-
如果 x 变了:重新算一次并更新缓存
-
一句话:依赖不变 → 复用旧结果;依赖变化 → 重新计算。
2) 用途 A:优化“很慢的计算”
const doubled = useMemo(() => slowDouble(number), [number])
效果:
-
改 number → 需要重新算(合理)
-
切换主题 dark → 不用重新算(省掉卡顿)
✅ 什么时候值得用?
-
计算真的慢(大循环、复杂过滤排序、图表数据处理、加密/哈希、复杂格式化)
-
或者计算量随数据规模变大很明显(比如 1w 条列表的 filter + sort)
3) 用途 B:解决“引用每次变导致 useEffect 乱触发”
JS 里对象/数组比较的是 引用地址,不是内容。
问题例子
const themeStyle = { color: dark ? "white" : "black" }
useEffect(() => {
console.log("theme changed")
}, [themeStyle])
每次渲染都会创建一个新对象 {...},引用永远不一样 → effect 每次都触发。
用 useMemo 修
const themeStyle = useMemo(
() => ({ color: dark ? "white" : "black" }),
[dark]
)
这样只有 dark 变,themeStyle 的引用才变,effect 才会触发。
你没理解的点其实就一个:useEffect 的依赖比较方式。
React 判断依赖变没变,不是看“内容一样不一样”,而是看是不是同一个引用(内存地址)。
4) 没有 useMemo:为什么会“乱触发”?
const config = { chainId, address }
useEffect(() => {
console.log("run")
}, [config])
关键:{ chainId, address } 这一行 每次渲染都会创建一个全新的对象。
即使 chainId 和 address 都没变,config 也是“新对象”。
你可以想象成这样:
-
第1次渲染:config 指向 0xAAA
-
第2次渲染:又 new 了一个对象,config 指向 0xBBB
-
第3次渲染:又 new 了一个对象,config 指向 0xCCC
React 看依赖 [config]:
“咦,引用从 0xAAA 变成 0xBBB 了 → 依赖变了 → effect 执行。”
所以你会看到:只要组件重新渲染一次(哪怕因为 theme、输入框、别的 state),effect 就会跑一次。
5) 有 useMemo:为什么就不乱触发?
const config = useMemo(() => ({ chainId, address }), [chainId, address])
useEffect(() => {
console.log("run only when chainId/address changes")
}, [config])
useMemo 做的事:缓存上一次创建的对象。
-
如果 chainId/address 没变:useMemo 直接把上一次那个对象返回给你(引用不变)
-
如果 chainId/address 变了:useMemo 才会创建新对象(引用改变)
所以变成:
-
第1次渲染:config 指向 0xAAA
-
第2次渲染(theme 变了,但 chainId/address 没变):useMemo 返回旧的 0xAAA
-
第3次渲染(别的 state 变了):还是 0xAAA
-
直到某次 chainId/address 变了:才生成 0xBBB
React 看依赖 [config]:
“引用没变 → 依赖没变 → effect 不执行。”
6) 用一句超级直观的话总结差别
-
没有 useMemo:每次渲染都“new 一个 config” → useEffect 觉得依赖变了 → 每次都跑
-
有 useMemo:只有当 chainId/address 变时才“new config” → useEffect 只在真正变化时跑
7) 你可能会问:那我为什么不直接写 [chainId, address] ?
对,这个问题问得很对。
很多时候最简单就是这样:
useEffect(() => {
console.log("run only when chainId/address changes")
}, [chainId, address])
那为什么还要 config + useMemo?
常见原因:
-
你下游需要一个 config 对象传给别的 hook / 子组件(比如 wagmi/viem)
-
依赖项很多,你想统一打包成一个对象(方便维护)
-
子组件是 React.memo,你需要保证 props 引用稳定
8) 一个最能让你“秒懂”的类比
-
没有 useMemo:你每次渲染都“重新打印一张身份证”,身份证号码当然变 → 系统认为换人了 → effect 触发
-
有 useMemo:只要信息没变,你就一直用同一张身份证 → 系统认为还是同一个人 → effect 不触发
9) useMemo vs useCallback:别混
-
useMemo:缓存“值”
-
useCallback:缓存“函数”
其实你可以理解为:
useCallback(fn, deps) === useMemo(() => fn, deps)
一个缓存结果,一个缓存函数引用。
10) 实战
下面介绍 6 个真实项目里最常见、而且“用不用 useMemo 差别非常明显”的例子(每个都说明:不用会怎样,用了会怎样)。
例子 1:对象作为 useEffect 依赖导致重复请求(拉余额/交易)
不用 useMemo(会重复请求)
const params = { address, chainId }
useEffect(() => {
fetchBalance(params) // 可能每次渲染都请求
}, [params])
只要组件因为别的 state 改了(主题、输入框、弹窗),params 就是新对象 → effect 又跑 → 又请求。
用 useMemo(只在 address/chainId 变时请求)
const params = useMemo(() => ({ address, chainId }), [address, chainId])
useEffect(() => {
fetchBalance(params)
}, [params])
例子 2:给 wagmi/viem 传 config,对象不稳定导致 hook 重复执行/重复订阅
很多 Web3 hook 内部会看 config 是否变化来重建 watcher。
不用 useMemo(可能反复重建 watcher / 重跑)
const config = { address, abi, functionName: "balanceOf", args: [user] }
useReadContract(config)
用 useMemo(config 稳定,只有关键参数变才重建)
const config = useMemo(() => ({
address,
abi,
functionName: "balanceOf",
args: [user],
}), [address, abi, user])
useReadContract(config)
例子 3:列表过滤/排序很大,导致 UI 卡(交易列表、NFT 列表)
不用 useMemo(任何状态变化都重新 filter/sort)
const visibleTxs = txs
.filter(tx => tx.chainId === chainId)
.sort((a,b) => b.time - a.time)
你一切换 theme、打开弹窗、输入搜索框,都会重新跑这套逻辑 → 卡。
用 useMemo(只在 txs 或 chainId 变时重算)
const visibleTxs = useMemo(() => {
return txs
.filter(tx => tx.chainId === chainId)
.sort((a,b) => b.time - a.time)
}, [txs, chainId])
例子 4:把 derived data(派生数据)传给 React.memo 子组件,避免子组件无意义重渲
不用 useMemo(子组件每次都渲染)
const columns = [
{ key: "hash", title: "Hash" },
{ key: "status", title: "Status" },
]
return <Table columns={columns} /> // Table 即使 memo,也会因为 columns 引用变而重渲
用 useMemo(columns 引用稳定,Table 不会乱渲)
const columns = useMemo(() => ([
{ key: "hash", title: "Hash" },
{ key: "status", title: "Status" },
]), [])
return <Table columns={columns} />
例子 5:useEffect 依赖一个 options 对象(比如图表/编辑器/SDK 初始化),导致重复初始化
比如初始化一个 chart/播放器/websocket SDK。
不用 useMemo(effect 每次 render 都 cleanup + init)
const options = { theme: dark ? "dark" : "light", locale }
useEffect(() => {
const chart = createChart(options)
return () => chart.destroy()
}, [options])
用 useMemo(只在 theme/locale 真变时重建)
const options = useMemo(() => ({
theme: dark ? "dark" : "light",
locale,
}), [dark, locale])
useEffect(() => {
const chart = createChart(options)
return () => chart.destroy()
}, [options])
例子 6:依赖数组(args)不稳定导致 effect/hook 频繁触发
不用 useMemo(args 每次 render 都是新数组)
const args = [spender, amount]
useEffect(() => {
prepareTx(args)
}, [args])
用 useMemo(args 稳定)
const args = useMemo(() => [spender, amount], [spender, amount])
useEffect(() => {
prepareTx(args)
}, [args])
你该怎么判断“到底要不要用 useMemo”
✅ 用在这两类上最值:
-
你把对象/数组/函数放进依赖数组(useEffect/useMemo/useCallback deps)
-
你做了重计算(filter/sort/map 大数据/复杂转换)
❌ 别用在:
-
简单计算(a+b、拼字符串)
-
没有依赖问题也没有卡顿问题的地方
11) 什么时候别用?
useMemo 不是“越多越好”,它本身也有开销(要维护缓存、比较 deps)。
别用在:
-
计算很轻(a+b、简单 map)
-
你根本没遇到性能问题/重复触发问题
-
deps 写不对反而更难 debug
12) 总结
useMemo = 缓存一个值(尤其是慢计算结果 或 需要稳定引用的对象/数组),依赖不变就复用。
更多推荐



所有评论(0)