先记住这个答案
Provider 用 Object.is 判断前后 value 是否变化。内联对象即使字段内容相同,每次创建也有不同引用,可能让消费者收到一次本来没有必要的 Context 更新。用 useMemo 构造对象,可以在依赖未变时复用其引用;当主题、用户或其它实际依赖改变时,再生成新值。这个缓存是性能优化,依赖必须完整,不能通过空数组把动态数据固定在初始快照。
- 对象内容相同不代表 Object.is 相同
- useMemo 复用的是计算结果,需要完整依赖
- value 稳定与子组件不因父层渲染执行是两件事
为什么包装对象让无关更新变成 Context 更新
父组件同时管理主题和点击计数。如果每次渲染都提供包含 theme 的新对象,增加计数也会创建一个新的主题包装对象。消费者无法把它当作上一份 value,即使其中的 theme 字符串未变。这里增加的不是业务数据变化,而是包装对象身份变化。
只传 theme 字符串就没有这层对象身份问题;需要组合多个字段时,再考虑缓存这个组合值。若其中包含函数,函数本身的身份和闭包依赖也会影响缓存。不能只包最外层 useMemo,却在每次渲染中新建一个作为依赖的函数,然后期待对象总被复用。
完整示例:计数变化与主题变化分开观察
下面的 ThemeLabel 使用 memo,便于区分 props 路径与 Context 路径;Provider value 用 useMemo 依赖 theme。增加计数只改变 App 的其它状态,切换主题则确实改变消费者需要的数据。示例关注可见主题与输入关系,不把开发模式的函数调用次数作为精确输出承诺。
实际项目中应围绕耗时组件测量,而不是因为这个简短标签就到处增加缓存。若移除 memo,标签仍可能随父层渲染执行;若移除 useMemo,新的对象又可能触发 Context 更新。两个优化分别处理不同来源,去掉任何一个都不应该使业务显示错误。
import { createContext, memo, useContext, useMemo, useState } from 'react';
const ThemeContext = createContext(null);
const ThemeLabel = memo(function ThemeLabel() {
const { theme } = useContext(ThemeContext);
return <p>当前主题:{theme}</p>;
});
export default function App() {
const [theme, setTheme] = useState('light');
const [count, setCount] = useState(0);
const value = useMemo(() => ({ theme }), [theme]);
return (
<ThemeContext.Provider value={value}>
<button onClick={() => setCount(c => c + 1)}>计数:{count}</button>
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
切换主题
</button>
<ThemeLabel />
</ThemeContext.Provider>
);
}放入 React 客户端组件环境后,计数按钮只增加数字,主题按钮在 light 与 dark 间切换。代码没有固定的控制台输出,也不以开发模式的渲染次数作为验收值。
依赖与缓存边界决定优化是否可信
假设数据 Context 提供 data、isLoading 和 refetch,那么构造这个对象的缓存必须处理这三个输入,而不能漏掉 refetch。若 refetch 因参数变化需要更新,消费者就应获得新的函数;为了稳定引用忽略参数,可能让刷新按钮继续请求上一个页面的数据。
useMemo 不负责永久保存资源,也不保证缓存永不被丢弃。页面逻辑应在重新计算时仍正确。启用 React Compiler 的工程可能减少手动缓存需求,但这取决于实际配置与编译结果,不能把 React 版本升级等同于已经自动优化了所有 Provider。
容易答错的地方
- 空依赖能让 Context 永远不重渲染,所以最好
- 若对象读取动态 state 或 props,空依赖会保留旧值并让界面失去更新。缓存依赖描述数据关系,不是人为压低渲染次数的按钮。
- useMemo 不能缓存函数,必须用 useCallback
- useMemo 可以返回一个函数,useCallback 则提供更直接的函数缓存写法。两者都需要正确依赖;区别不能简化成一个允许函数、另一个禁止函数。
面试官还会怎么问?
只要 Context 用对象就必须缓存吗?
不必须。若更新少、消费者轻量,缓存管理成本可能高于收益;也可能对象本来就来自未变化的 state 引用。先确认有无不必要的新引用和明显的重复工作。
缓存后某个字段改变,其他消费者还会更新吗?
会,依赖变化会构造新的整个 value,内置 useContext 不会自动按字段订阅。若不同字段的消费范围差异明显,可以进一步考虑拆分通道。
可以把动态配置移到模块顶层来稳定引用吗?
真正静态的配置可以这样组织。依赖用户、请求或组件实例的数据不能随意改成共享模块对象,否则可能混淆不同实例的状态与生命周期。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。