学完这篇,你能判断什么时候该用 React Context、写出不会踩坑的 Provider,并在页面卡顿时知道该改哪一行。
Context 不是"全局状态管理"的同义词,它解决的是跨层级传参这一个问题。下面按判断、落地、优化的顺序讲。
第一步:先判断该不该用
满足以下任一条,才值得开 Context:
- 同一份数据要被三层以上的组件读到(主题、语言、登录用户、权限)。
- 数据生命周期长,不随某个局部交互频繁抖动。
- 你受够了把
user 从 <App> 一路传到 <Avatar> 的 props drilling。
不该用的情况也很明确:只有一个子组件需要、或者状态只在两三个相邻组件间共享——直接传 props 更清楚。
注意:如果你的数据每秒更新几十次(鼠标位置、滚动进度、实时行情),Context 会让所有消费者一起重渲染,这不是它的强项,用订阅式方案(useSyncExternalStore、zustand)更合适。
第二步:写出最小的可用版本
// ThemeContext.jsx
import { createContext, useContext, useState, useMemo } from 'react';
const ThemeContext = createContext(null); // 默认值给 null,方便下面校验
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}
export function useTheme() {
const ctx = useContext(ThemeContext);
if (ctx === null) {
throw new Error('useTheme 必须在 ThemeProvider 内使用');
}
return ctx;
}
要点有三个:
- 导出封装好的
useTheme,而不是把 Context 对象暴露出去,调用方永远不 import Context 本身。
- 默认值给
null 并抛错。如果默认值写成一个假的 {theme:'light'},Provider 忘了包也不会报错,你会在几小时后才发现主题不生效。
- value 用
useMemo 包住。这一步是后面所有优化的地基。
注意:<ThemeContext.Provider value={{ theme, setTheme }}> 这种写法每次渲染都会新建对象,引用变化 → 全量消费者重渲染。useMemo 的依赖数组要写全,漏了 theme 会出现值不更新的诡异 bug。
第三步:拆分 Context,别做"万能大对象"
一个 Context 只放一类数据。把"用户信息 + 主题 + 通知 + 购物车"塞进一个 AppContext,会导致改个主题就让所有读用户的组件跟着重渲染。
更进一步,把不变的部分和会变的部分拆开:
const UserContext = createContext(null); // 用户对象,登录后基本不变
const DispatchContext = createContext(null); // setUser 等函数,引用永远稳定
setState 函数引用天然稳定,把它单独放一个 Context,只调用 actions、不读数据的组件就永远不会因为数据变化而重渲染。
第四步:性能优化三板斧
① 拆 Context(如上) —— 收益最大,成本最低。
② value 稳定化 —— useMemo / useCallback,把对象和数组都包住。
③ 消费者用 memo —— Context 变化时,Provider 的所有子组件都会重新渲染(不是只有消费者)。如果子树很重,把它抽成组件并用 React.memo 包住:
const HeavyList = React.memo(function HeavyList() {
const { theme } = useTheme();
return <div className={theme}>{/* 大量节点 */}</div>;
});
更实用的组合技巧是把 children 提上去:<Provider>{children}</Provider> 中的 children 是父组件传进来的元素,Provider 自己 setState 时这个元素引用不变,React 会直接跳过它的重渲染。
注意:Context 值变化时,React 会从 Provider 往下重新渲染所有消费者,React.memo 拦不住(memo 比较的是 props,而 useContext 的订阅绕过了 props)。真正拦住的是"不消费 Context 的中间层",别指望 memo 能救消费者本身。
如果确实需要"只订阅 Context 的一部分",用 use-context-selector 这类库,基于 useSyncExternalStore 实现细粒度订阅;或者自己用 reducer + 订阅模式手写。规模不大的项目没必要上。
第五步:什么时候别硬上 Context
- 需要时间旅行调试、复杂中间件、跨模块高频写入 → Redux Toolkit / zustand。
- 服务端数据(列表、详情、缓存、重试) → 用 TanStack Query 这类请求库,别塞进 Context 手写 loading/error。
- 表单 → react-hook-form 这类,Context 传整个表单状态会失控。
一句话:Context 管低频、跨层、只读为主的共享状态;请求缓存交给请求库,高频状态交给订阅式 store。
小结
- Context 解决的是跨层级传参,不是通用状态管理,先判断再动手。
- 永远导出
useXxx 自定义 hook,默认值给 null 并抛错,避免静默失效。
value 必须 useMemo,依赖数组写全。
- 一个 Context 只放一类数据;读数据和改数据拆成两个 Context。
- 高频更新、需要 selector 的场景换方案,别跟 Context 硬碰。
- 重子树用
React.memo + children 提升来隔离,但记住 memo 拦不住 Context 消费者。