陌上人如玉
公子世无双

3.3 Context 与 useContext

3.3 Context 与 useContext

一句话: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 (
    
      
{ e.preventDefault(); onSubmit(values); }}>{children}
); } // 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 里收窄)

八、面试延伸

  1. 「Context 解决了什么问题?」
    解决「props 层层透传(props drilling)」的问题:当某个数据需要被「跨多层」的后代读取,而中间层完全不关心它时,用 props 传递会产生大量样板代码与耦合。Context 提供了一根「看不见的管道」——祖先投放(Provider)、后代取用(useContext),中间层不用参与。典型场景是主题、语言、当前用户、权限、以及组件库里的「组合式上下文」(Form ↔ FormItem)。

  2. 「Context 是状态管理方案吗?」
    不是。Context 只解决「读取」(跨层级的依赖注入),它不提供「状态如何更新、如何派生、如何订阅其中一部分」的能力。所以实践中 Context 通常配合 useState / useReducer 使用(把「值」与「dispatch」放进 Context),或者只用来注入一个「外部 store 的实例」。真正的状态管理方案是 Redux / Zustand / Jotai 这类库。

  3. 「Context 和 props 怎么选?」
    三个判断维度:① 层级深度——1~2 层用 props(更显式、类型清晰);3 层以上且中间层不关心才考虑 Context。② 变化频率——低频(主题/用户)用 Context;高频(输入值、鼠标位置)用带选择器的状态库(因为 Context 的 value 一变会让所有消费者重渲染)。③ 使用方数量与分布——如果一个数据只有一个分支用,继续 props 也许更简单。

  4. 「Context 的默认值有什么用?」
    当组件没有被任何 Provider 包裹时,useContext 会返回 createContext(默认值) 里的默认值。实践中推荐「默认值设为 null + 封装一个 useXxx() Hook 里判空抛错」——这样能尽早暴露「忘记包 Provider」的错误。如果给一个「有意义的默认值」,使用方虽然不需要判空,但也会掩盖配置错误(比如主题始终显示默认值而无人察觉)。

  5. 「Provider 嵌套时哪个生效?」
    最近的生效。多个同类型 Provider 嵌套时,useContext 会取「向上查找遇到的第一层 Provider」的值。注意一个细节:Provider 显式传 undefined 时读到的是 undefined,而不是 createContext 的默认值——这与 props 不同(props 传 undefined 会触发默认参数)。要「回到默认值」必须移除 Provider。

  6. 「Context 值的变化为什么会让所有消费者重渲染?」
    因为 Context 的订阅粒度是整个 value:Provider 用 Object.is 比较新旧 value,一旦不等就通知所有订阅该 Context 的组件重新渲染,不管它们实际只用到 value 的哪一部分。而且这个通知无法被 React.memo 拦截(context 传播不经过 props 比较)。所以「把高频变化的值放进 Context」会造成大范围重渲染——这是 Context 最需要注意的性能问题。

  7. 「怎么封装一个好用的 Context?」
    推荐「三文件模式」:① xxx-context.ts——只放 createContext 与类型(避免循环依赖);② xxx-provider.tsx——Provider 组件(内部管理状态,用 useMemo 稳定 value);③ use-xxx.ts——自定义 Hook(判空、抛错、返回收窄后的类型)。使用时只需 useTheme(),使用方完全不需要接触 useContext 与 null 判断,API 更简洁、错误更早暴露。

  8. 「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 更好。

赞(0) 打赏
未经允许不得转载:陌上寒 » 3.3 Context 与 useContext

评论 抢沙发

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

微信扫一扫

支付宝扫一扫