
一句话:
useContext让组件跨越任意层级读取共享值,避免「层层透传 props」;但它的代价是「Provider 的 value 一变,所有消费者都会重渲染」——所以它适合「不常变化的共享值」,不适合高频变化的状态。
一、三步用法
// ① 创建 Context(通常在单独的文件里)
// theme-context.ts
import { createContext } from 'react';
export interface ThemeContextValue {
theme: 'light' | 'dark';
toggle: () => void;
}
export const ThemeContext = createContext(null);
// ② 提供值(在祖先组件里)
import { ThemeContext } from './theme-context';
function App() {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
// ⚠️ value 每次渲染都是新对象 → 所有消费者都会重渲染
const value = { theme, toggle: () => setTheme((t) => (t === 'light' ? 'dark' : 'light')) };
// ✅ 用 useMemo 稳定(但依赖 theme 变化时仍会变)
const stableValue = useMemo(
() => ({ theme, toggle: () => setTheme((t) => (t === 'light' ? 'dark' : 'light')) }),
[theme]
);
return (
);
}
// ③ 消费(任意深度的后代)
import { useContext } from 'react';
import { ThemeContext } from './theme-context';
function DeepChild() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error('DeepChild 必须在 ThemeContext.Provider 内使用');
return ;
}
二、默认值
// createContext 的参数是「没有 Provider 时的默认值」
const Ctx = createContext(null);
// ① 用 null 作默认值 → 强制使用方处理「没有 Provider」的情况
const value = useContext(Ctx);
if (value === null) throw new Error('必须在 Provider 内使用');
// ② 提供一个「有意义的默认值」(适合有合理默认行为的场景)
const ThemeCtx = createContext({
theme: 'light',
toggle: () => {},
});
// ✅ 更推荐的写法:封装成自定义 Hook(把错误处理统一起来)
export function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error('useTheme 必须在 ThemeProvider 内使用');
return ctx;
}
| 默认值策略 | 优点 | 缺点 |
|---|---|---|
null + 使用时判空 |
强制处理缺失情况 | 每次都要判空 |
| 有意义的默认值 | 使用方无需判空 | 掩盖「忘记包 Provider」的错误 |
封装成 useXxx() Hook |
统一错误处理,使用方最简洁 | 多写一个函数 |
三、Provider 的写法演进
// React 18 及之前:必须用 Context.Provider
// React 19+:可以直接把 Context 当 Provider 用
// 两种写法都可以(Provider 仍然兼容)
四、重渲染行为(最重要的部分)
const Ctx = createContext({ a: 0 });
function Provider() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
// ⚠️ 每次渲染都是新对象 → 即使 a 没变,所有消费者也会重渲染
return (
);
}
function Consumer() {
const { a } = useContext(Ctx);
console.log('Consumer 渲染'); // ⚠️ 点「改 b」时也会渲染
return {a};
}
为什么?
Context 的更新机制是「按 Context 对象订阅」,粒度是【整个 value】:
① Provider 的 value 用 Object.is 比较,变了就通知【所有】消费者
② 消费者组件收到通知后重新渲染——不管它实际用到 value 的哪一部分
③ 而且【无法用 React.memo 或选择性订阅来规避】(这就是 Context 的局限)
所以:
· value 里放了「会频繁变化的字段」→ 所有消费者频繁重渲染
· value 是「每次渲染新建的对象字面量」→ 即使内容没变也会全部重渲染(引用变了)
五、什么时候用 Context
适合:
① 跨多层级的「全局但低频变化」的数据
· 主题(light/dark)
· 语言 / 国际化文案
· 当前用户信息(登录后变化很少)
· 权限配置
· 路由信息(Router 内部就用 Context)
② 组件的「组合式上下文」(如 Form 与 FormItem 之间的通信)
③ 避免「层层透传 props」的样板代码
不适合:
① 高频变化的状态(如输入框的实时值、鼠标位置、动画进度)
→ 会导致大范围重渲染,应该用状态管理库(Zustand/Jotai)或 props
② 只有两层传递的场景(直接传 props 更清晰)
③ 「服务端数据缓存」(用 TanStack Query)
④ 需要「选择器订阅」(只关心 value 的一部分)的场景
| 场景 | 推荐 |
|---|---|
| 主题 / 语言 / 用户 | Context |
| 表单上下文(Form ↔ FormItem) | Context |
| 路由(内部实现) | Context |
| 全局计数器 / 高频状态 | Zustand / Jotai |
| 接口数据 | TanStack Query / SWR |
| 父子两三层传递 | props |
六、与 props 的取舍
// ❌ props 透传:中间层「知道但不用」的数据
function App({ user }) {
return ;
}
function Layout({ user }) {
return ; // Layout 根本不用 user
}
function Header({ user }) {
return ;
}
function Avatar({ user }) {
return
;
}
// ✅ 用 Context 消除中间层
const UserContext = createContext(null);
function App() {
const user = useUser();
return (
);
}
function Layout() {
return (
{/* 需要时自己取 */}
);
}
function Avatar() {
const user = useUserContext();
return
;
}
取舍建议:
① 只透传 1~2 层 → 直接用 props(更显式、更好测试、类型更清楚)
② 透传 3 层以上,且中间层完全不关心 → 用 Context
③ Context 不是「状态管理方案」——它只解决「跨层级读取」,
状态本身的更新与派生仍然要自己管理(或交给状态库)
七、性能优化(拆分为多个 Context)
// ❌ 把所有东西塞进一个 Context → 任一字段变化都会让所有消费者重渲染
const AppContext = createContext({ user: null, theme: 'light', cart: [] });
// ✅ 按「变化频率」与「使用方」拆分
const UserContext = createContext(null); // 很少变
const ThemeContext = createContext('light'); // 很少变
const CartContext = createContext(null); // 经常变
// 使用方只订阅自己需要的 Context
// → cart 变化不会让「只读 theme」的组件重渲染
// ✅ 另一个技巧:把「稳定的方法」与「变化的值」拆成两个 Context
const StateContext = createContext(initial); // 会变
const ActionsContext = createContext(null); // 不变(可以永不更新)
// → 只需要 actions 的组件永远不会因为 state 变化而重渲染
// ✅ 用「children 透传」减少 Provider 重渲染的影响
function Provider({ children }: { children: React.ReactNode }) {
const [state, setState] = useState(initial);
return (
{children} {/* ⭐ children 是「外部传入的元素」,不会因 state 变化而重建 */}
);
}
八、常见坑
// ① value 是对象字面量 → 每次渲染都通知所有消费者
// ✅ 用 useMemo 或把值拆开
({ a, b }), [a, b])}>
// ② 把「高频变化的值」放 Context
// ⚠️ 鼠标一动就全量重渲染
// ③ 以为 React.memo 能阻止 Context 引起的重渲染
const Consumer = React.memo(function Consumer() {
const { a } = useContext(Ctx); // ⚠️ memo 无效:context 更新会穿透
});
// ④ 忘记 Provider 导致拿到默认值(且错误难以察觉)
// ✅ 用「封装 Hook + 判空抛错」把错误提前
// ⑤ 在 Provider 之外调用 useContext(如事件回调里的组件树已卸载)
// ⑥ 多个同类型 Context 嵌套时,就近的生效(容易混淆)
// ⑦ 用 Context 传递「不需要跨层级」的数据(过度设计)
// ⑧ 用 Context 当状态管理(Context 只解决「读取」,不解决「更新与派生」)
// ⑨ value 里放了「每次渲染新建的函数」→ 引用变化导致全量重渲染
// ✅ 用 useCallback 包装函数
// ⑩ 一个组件里 useContext 多个 Context → 任一变化都会让它重渲染
面试延伸
- 「Context 解决了什么问题?」
解决「跨层级共享数据」的问题——即「props 逐层透传(props drilling)」带来的样板代码与耦合。典型场景是主题、语言、当前用户、权限配置这类「全局但低频变化」的数据。注意 Context 只解决「读取」,不解决「状态如何更新与派生」——后者仍然需要useState/useReducer或状态管理库。 -
「Context 的默认值有什么用?」
它在「组件没有被任何 Provider 包裹」时生效。实践中推荐「默认值设为null(或一个哨兵值)+ 在自定义 Hook 里判空抛错」——这样能尽早发现「忘记包 Provider」的错误。如果给一个「有意义的默认值」,虽然使用方不需要判空,但也可能掩盖配置错误(比如主题始终是默认值而不知道)。 -
「为什么 Context 的 value 变化会让所有消费者重渲染?」
因为 Context 的订阅粒度是整个value:Provider 用Object.is比较新旧 value,一旦不等就通知所有订阅该 Context 的组件重新渲染——不管它们实际只用到 value 的哪一部分。而且这个通知无法被React.memo阻止(context 更新会穿透 memo)。所以把「高频变化的值」放进 Context 会造成大范围重渲染。 -
「怎么优化 Context 的重渲染?」
四种手段:① 拆分 Context——按「变化频率」或「使用方」拆成多个(甚至把「变化的值」与「稳定的方法」拆开:actions Context 永不更新);②useMemo包装 value——避免「内容没变但引用变了」导致的伪更新;③children透传——把「不依赖 context 的子树」作为children传入,它不会因 Provider 重渲染而重建;④ 改用支持选择器订阅的状态库(Zustand / Jotai /use-context-selector)——只订阅 value 的一部分。 -
「Context 和状态管理库有什么区别?」
Context 是「依赖注入 / 跨层级读取」的机制,它没有「状态更新、选择器订阅、中间件、DevTools、时间旅行」这些能力;状态管理库(Redux/Zustand/Jotai)才是「状态管理方案」。实践中经常结合使用:把 store 实例通过 Context 注入(避免全局单例),而订阅与更新由库自己处理——这样既能跨层级访问,又有选择器订阅的性能优势。 -
「Context 适合存什么数据?」
三条判断标准:① 跨多层组件(否则直接用 props);② 变化频率低(否则会造成大范围重渲染);③ 多个组件都需要读取(否则没必要共享)。符合这三条的典型是「主题、语言、用户、权限、路由信息、表单上下文」。反例是「输入框实时值、鼠标位置、动画进度、接口数据」。 -
「
React.memo能阻止 Context 变化引起的重渲染吗?」
不能。React.memo只比较 props;而 Context 的更新是直接通知消费者组件的(通过内部的订阅机制),不经过 props 比较环节,因此 memo 无法拦截。这也是「Context 性能问题」比较棘手的原因——解法只能是「拆分 Context」「用选择器订阅的库」或在结构上避免让高频数据进入 Context。 -
「怎么设计一个 Context 才能兼顾易用与性能?」
三条经验:① 一个 Context 只放一类数据(按变化频率与使用方拆分);② 把「值」与「操作」拆成两个 Context——StateContext(会变)与ActionsContext(永不更新),这样只用 actions 的组件(如表单提交按钮)永远不会重渲染;③ 提供一个自定义 Hook 封装读取(useXxx()),统一处理「Provider 缺失」与类型断言,使用方只关心业务语义。
一句话速记
Context 解决「跨层级读取」(不是状态管理);用法三步:
createContext→<Ctx.Provider value>→useContext(推荐封装成useXxx()+ 判空抛错);value 一变,所有消费者都重渲染(React.memo拦不住),所以它只适合「不常变化的共享值」(主题/语言/用户/权限);优化靠「拆分 Context(值与方法分开)+useMemo包装 value +children透传」;需要「选择器订阅」或高频更新就用状态管理库。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!