
一句话:Context 是 React 提供的「依赖注入 / 跨层级读取」机制——它让你把值放在祖先组件上,任意深度的后代都能直接读取,从而消除「props 层层透传」。
一、Context 解决什么问题
// ❌ 问题:props drilling(层层透传)
function App() {
const theme = useThemeValue(); // 'light' | 'dark'
return ;
}
function Layout({ theme }) {
return ; // Layout 根本不用 theme,只是转发
}
function Header({ theme }) {
return ; // Header 也不用
}
function NavBar({ theme }) {
return ;
}
function ThemeToggle({ theme }) {
return ; // 只有这里真正需要
}
// ✅ 用 Context:需要的地方直接读,中间层完全不用管
const ThemeContext = createContext<'light' | 'dark'>('light');
function App2() {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const value = useMemo(() => ({ theme, toggle: () => setTheme((t) => (t === 'light' ? 'dark' : 'light')) }), [theme]);
return (
);
}
function NavBar2() {
const { theme, toggle } = useContext(ThemeContext); // ⭐ 想读就读,不用透传
return ;
}
Context 的本质:
它是一根「看不见的管道」:
① 祖先组件把值「投放」到管道里(Provider)
② 任意深度的后代可以「从管道里取」(useContext)
③ 中间层即使存在,也不会被 props 污染
⚠️ 关键限制:Context 的更新【不经过 props 比较】,
所以它「绕过」了 React.memo —— 这是它性能问题的根源(见 3.4)。
二、三步使用(配合 TypeScript)
// ① 创建 + 导出(单独一个文件,避免循环依赖)
// contexts/theme-context.ts
import { createContext } from 'react';
export interface ThemeContextValue {
theme: 'light' | 'dark';
toggle: () => void;
}
export const ThemeContext = createContext(null);
// ↑ 默认值用 null,强制使用方处理「没有 Provider」的情况
// ② 提供一个「Provider 组件 + 自定义 Hook」(推荐的封装方式)
// contexts/theme-provider.tsx
import { useCallback, useMemo, useState, type ReactNode } from 'react';
import { ThemeContext, type ThemeContextValue } from './theme-context';
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const toggle = useCallback(() => {
setTheme((t) => (t === 'light' ? 'dark' : 'light'));
}, []);
// ⭐ value 用 useMemo 稳定(theme 不变时引用不变)
const value = useMemo(() => ({ theme, toggle }), [theme, toggle]);
return {children} ;
}
// ③ 消费:用自定义 Hook(统一判空与类型收窄)
// contexts/use-theme.ts
import { useContext } from 'react';
import { ThemeContext } from './theme-context';
export function useTheme(): ThemeContextValue {
const ctx = useContext(ThemeContext);
if (!ctx) {
throw new Error('useTheme 必须在 内部使用');
}
return ctx;
}
// 使用:调用方极其简洁,且错误在开发时就暴露
function ThemeToggle() {
const { theme, toggle } = useTheme();
return ;
}
| 封装方式 | 使用方代码 | 错误处理 |
|---|---|---|
直接用 useContext |
const ctx = useContext(Ctx); ctx!.theme |
自己判空 |
封装成 useXxx() |
const { theme } = useTheme() |
统一在 Hook 里抛错 ✅ |
三、嵌套与覆盖
const Ctx = createContext('default');
function App() {
return (
<>
{/* 显示 'outer' */}
{/* ⭐ 显示 'inner'(就近的生效) */}
{/* 显示 'outer' */}
{/* ⚠️ value={undefined} 会导致 useContext 返回【默认值 'default'】? */}
{/* 实际上:Provider 提供了值(undefined),但 useContext 会读到 undefined */}
{/* 显示 undefined(不是 'default') */}
{/* 显示 'default'(没有 Provider) */}
>
);
}
function Show() {
const value = useContext(Ctx);
return {String(value)};
}
嵌套规则:
① 多个同类型 Provider 嵌套时,【最近的】生效
② 没有 Provider 时,useContext 返回【createContext 的默认值】
③ Provider 显式传 undefined 时,读到的是 undefined(不是默认值)
⚠️ 这一点与「props 的默认值」不同:props 传 undefined 会触发默认值,
而 Context 传 undefined 就是 undefined
④ 不同 Context 之间互不影响(可以嵌套多层,各自独立)
四、默认值的三种策略
// ① 用 null 作默认值(推荐)→ 强制使用方判空
const Ctx1 = createContext(null);
// 配合封装 Hook 里的判空抛错
// ② 提供「有意义的默认值」→ 使用方无需判空
// 适合「即使没有 Provider 也有合理行为」的场景
const Ctx2 = createContext({
theme: 'light',
toggle: () => {
console.warn('没有 ThemeProvider,toggle 无效');
},
});
// ⚠️ 风险:忘记包 Provider 时不会报错,只是行为不生效(难以发现)
// ③ 提供「哨兵值」→ 用于区分「未提供」
const NOT_PROVIDED = Symbol('not-provided');
const Ctx3 = createContext(NOT_PROVIDED);
function useCtx3() {
const v = useContext(Ctx3);
if (v === NOT_PROVIDED) throw new Error('必须在 Provider 内使用');
return v;
}
| 策略 | 安全性 | 使用方体验 |
|---|---|---|
null + 判空 |
✅ 高(错误暴露早) | 需要判空(或用封装 Hook) |
| 有意义默认值 | ⚠️ 低(掩盖配置错误) | 简洁 |
| 哨兵值 | ✅ 高 | 需要封装 Hook |
五、什么时候用 Context(与 props 的取舍)
✅ 适合用 Context:
① 跨 3 层以上,且中间层完全不关心该数据
② 数据「低频变化」(主题、语言、当前用户、权限、路由信息)
③ 多个不同分支的组件都需要读取(不是「只有一条路径需要」)
④ 「组合式组件」的上下文(Form ↔ FormItem、Table ↔ TableColumn)
❌ 不适合:
① 只透传 1~2 层(props 更显式、更好测试)
② 数据「高频变化」(会导致大范围重渲染)→ 用状态库
③ 只有一两个组件需要(条件允许的话直接 props)
④ 服务端数据(用 TanStack Query)
⑤ 想用它做「状态管理」(它只解决读取,不解决更新与派生)
// 一个「组合式组件」的典型用法(组件库常用)
const FormContext = createContext<{
values: Record;
errors: Record;
setFieldValue: (name: string, value: unknown) => void;
} | null>(null);
export function Form({ children, onSubmit }: { children: ReactNode; onSubmit: (v: any) => void }) {
const [values, setValues] = useState({});
const [errors, setErrors] = useState({});
const setFieldValue = useCallback((name: string, value: unknown) => {
setValues((prev) => ({ ...prev, [name]: value }));
}, []);
const value = useMemo(() => ({ values, errors, setFieldValue }), [values, errors, setFieldValue]);
return (
);
}
// FormItem 从 Context 里拿「自己这个字段」的状态(不需要 props 透传)
export function FormItem({ name, children }: { name: string; children: ReactNode }) {
const ctx = useContext(FormContext);
if (!ctx) throw new Error('FormItem 必须在 Form 内使用');
return (
{children}
{ctx.errors[name] && {ctx.errors[name]}
}
);
}
六、其它常见模式
// ① 「服务/依赖注入」模式(把「能力」注入,便于测试时替换)
const ApiContext = createContext(null);
export function ApiProvider({ client, children }: { client: ApiClient; children: ReactNode }) {
return {children} ;
}
export function useApi() {
const client = useContext(ApiContext);
if (!client) throw new Error('useApi 必须在 ApiProvider 内使用');
return client;
}
// 测试时注入 mock client:
render(
);
// ② 「配置」模式(避免把配置一路透传)
const ConfigContext = createContext(null);
// ③ 「全局状态」模式(Context + useReducer,见 2.8)
// ④ 「国际化」模式(当前语言 + t 函数)
// ⑤ 「路由」模式(React Router 内部就是这么做的)
七、常见坑
// ① value 是对象字面量 → 每次渲染都通知所有消费者
// ❌
// ✅ 用 useMemo
// ② 忘记封装判空 → 出现「读不到值」的诡异 bug
const ctx = useContext(Ctx); // ⚠️ 可能是 null
ctx.value; // 💥
// ③ 用 null 作默认值但使用方没判空
// ④ 用「有意义的默认值」掩盖了「忘记包 Provider」的错误
// ⑤ 在同一个文件里同时定义 Context 与 Provider → 容易造成循环依赖
// ✅ Context 单独一个文件,Provider 与 Hook 各一个文件
// ⑥ 把「频繁变化的值」放进 Context
// ⑦ 一个巨型 Context 塞了所有全局状态
// ⑧ 用 Context 替代「应该直接传 props」的场景(过度设计)
// ⑨ 在「事件处理」里读取 Context
// ⚠️ 只能在组件函数体(或自定义 Hook)里调用 useContext
// ⑩ 认为 Context 的值「可以跨 React 树共享」
// → 它只在【组件树】里有效;如果组件被渲染到别的 root,就读不到
// ⑪ Provider 放在「不需要的层级」,导致整棵树重渲染
// ✅ 尽量把 Provider 放低(只包住真正需要的子树)
// ⑫ 忘记 value 的类型(TS 里用 null 时要在 Hook 里收窄)
八、面试延伸
- 「Context 解决了什么问题?」
解决「props 层层透传(props drilling)」的问题:当某个数据需要被「跨多层」的后代读取,而中间层完全不关心它时,用 props 传递会产生大量样板代码与耦合。Context 提供了一根「看不见的管道」——祖先投放(Provider)、后代取用(useContext),中间层不用参与。典型场景是主题、语言、当前用户、权限、以及组件库里的「组合式上下文」(Form ↔ FormItem)。 -
「Context 是状态管理方案吗?」
不是。Context 只解决「读取」(跨层级的依赖注入),它不提供「状态如何更新、如何派生、如何订阅其中一部分」的能力。所以实践中 Context 通常配合useState/useReducer使用(把「值」与「dispatch」放进 Context),或者只用来注入一个「外部 store 的实例」。真正的状态管理方案是 Redux / Zustand / Jotai 这类库。 -
「Context 和 props 怎么选?」
三个判断维度:① 层级深度——1~2 层用 props(更显式、类型清晰);3 层以上且中间层不关心才考虑 Context。② 变化频率——低频(主题/用户)用 Context;高频(输入值、鼠标位置)用带选择器的状态库(因为 Context 的 value 一变会让所有消费者重渲染)。③ 使用方数量与分布——如果一个数据只有一个分支用,继续 props 也许更简单。 -
「Context 的默认值有什么用?」
当组件没有被任何 Provider 包裹时,useContext会返回createContext(默认值)里的默认值。实践中推荐「默认值设为null+ 封装一个useXxx()Hook 里判空抛错」——这样能尽早暴露「忘记包 Provider」的错误。如果给一个「有意义的默认值」,使用方虽然不需要判空,但也会掩盖配置错误(比如主题始终显示默认值而无人察觉)。 -
「Provider 嵌套时哪个生效?」
最近的生效。多个同类型 Provider 嵌套时,useContext会取「向上查找遇到的第一层 Provider」的值。注意一个细节:Provider 显式传undefined时读到的是undefined,而不是 createContext 的默认值——这与 props 不同(props 传undefined会触发默认参数)。要「回到默认值」必须移除 Provider。 -
「Context 值的变化为什么会让所有消费者重渲染?」
因为 Context 的订阅粒度是整个value:Provider 用Object.is比较新旧 value,一旦不等就通知所有订阅该 Context 的组件重新渲染,不管它们实际只用到 value 的哪一部分。而且这个通知无法被React.memo拦截(context 传播不经过 props 比较)。所以「把高频变化的值放进 Context」会造成大范围重渲染——这是 Context 最需要注意的性能问题。 -
「怎么封装一个好用的 Context?」
推荐「三文件模式」:①xxx-context.ts——只放createContext与类型(避免循环依赖);②xxx-provider.tsx——Provider 组件(内部管理状态,用useMemo稳定 value);③use-xxx.ts——自定义 Hook(判空、抛错、返回收窄后的类型)。使用时只需useTheme(),使用方完全不需要接触useContext与null判断,API 更简洁、错误更早暴露。 -
「Context 能跨 React 树共享吗?」
不能。Context 的作用域是「组件树」——它通过组件的父子关系传递(内部用「栈」维护当前 Provider 的值)。因此:① 如果某个组件被渲染到另一个 root(createRoot),它读不到原树的 Context;② Portal 里的组件仍然能读到(因为 Portal 只改变 DOM 位置,组件树关系不变);③ 不同Provider分支之间互不影响(各自的子树读到各自的值)。
一句话速记
Context 是「依赖注入 / 跨层级读取」的机制(不是状态管理):
createContext→<Ctx.Provider value>→useContext;推荐用「三文件模式」(context 单独文件 + Provider 组件 +useXxx()判空 Hook),默认值用null+ 抛错(早暴露「忘包 Provider」);嵌套时最近的生效、没有 Provider 才用默认值(传undefined不等于默认值);只适合低频、跨 3 层以上的共享(高频会导致全量重渲染),1~2 层直接用 props 更好。



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