陌上人如玉
公子世无双

2.6 useContext

2.6 useContext

一句话: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 → 任一变化都会让它重渲染

面试延伸

  1. 「Context 解决了什么问题?」
    解决「跨层级共享数据」的问题——即「props 逐层透传(props drilling)」带来的样板代码与耦合。典型场景是主题、语言、当前用户、权限配置这类「全局但低频变化」的数据。注意 Context 只解决「读取」,不解决「状态如何更新与派生」——后者仍然需要 useState / useReducer 或状态管理库。

  2. 「Context 的默认值有什么用?」
    它在「组件没有被任何 Provider 包裹」时生效。实践中推荐「默认值设为 null(或一个哨兵值)+ 在自定义 Hook 里判空抛错」——这样能尽早发现「忘记包 Provider」的错误。如果给一个「有意义的默认值」,虽然使用方不需要判空,但也可能掩盖配置错误(比如主题始终是默认值而不知道)。

  3. 「为什么 Context 的 value 变化会让所有消费者重渲染?」
    因为 Context 的订阅粒度是整个 value:Provider 用 Object.is 比较新旧 value,一旦不等就通知所有订阅该 Context 的组件重新渲染——不管它们实际只用到 value 的哪一部分。而且这个通知无法被 React.memo 阻止(context 更新会穿透 memo)。所以把「高频变化的值」放进 Context 会造成大范围重渲染。

  4. 「怎么优化 Context 的重渲染?」
    四种手段:① 拆分 Context——按「变化频率」或「使用方」拆成多个(甚至把「变化的值」与「稳定的方法」拆开:actions Context 永不更新);② useMemo 包装 value——避免「内容没变但引用变了」导致的伪更新;③ children 透传——把「不依赖 context 的子树」作为 children 传入,它不会因 Provider 重渲染而重建;④ 改用支持选择器订阅的状态库(Zustand / Jotai / use-context-selector)——只订阅 value 的一部分。

  5. 「Context 和状态管理库有什么区别?」
    Context 是「依赖注入 / 跨层级读取」的机制,它没有「状态更新、选择器订阅、中间件、DevTools、时间旅行」这些能力;状态管理库(Redux/Zustand/Jotai)才是「状态管理方案」。实践中经常结合使用:把 store 实例通过 Context 注入(避免全局单例),而订阅与更新由库自己处理——这样既能跨层级访问,又有选择器订阅的性能优势。

  6. 「Context 适合存什么数据?」
    三条判断标准:① 跨多层组件(否则直接用 props);② 变化频率低(否则会造成大范围重渲染);③ 多个组件都需要读取(否则没必要共享)。符合这三条的典型是「主题、语言、用户、权限、路由信息、表单上下文」。反例是「输入框实时值、鼠标位置、动画进度、接口数据」。

  7. 「React.memo 能阻止 Context 变化引起的重渲染吗?」
    不能。React.memo 只比较 props;而 Context 的更新是直接通知消费者组件的(通过内部的订阅机制),不经过 props 比较环节,因此 memo 无法拦截。这也是「Context 性能问题」比较棘手的原因——解法只能是「拆分 Context」「用选择器订阅的库」或在结构上避免让高频数据进入 Context。

  8. 「怎么设计一个 Context 才能兼顾易用与性能?」
    三条经验:① 一个 Context 只放一类数据(按变化频率与使用方拆分);② 把「值」与「操作」拆成两个 Context——StateContext(会变)与 ActionsContext(永不更新),这样只用 actions 的组件(如表单提交按钮)永远不会重渲染;③ 提供一个自定义 Hook 封装读取(useXxx()),统一处理「Provider 缺失」与类型断言,使用方只关心业务语义。

一句话速记

Context 解决「跨层级读取」(不是状态管理);用法三步:createContext → <Ctx.Provider value> → useContext(推荐封装成 useXxx() + 判空抛错);value 一变,所有消费者都重渲染(React.memo 拦不住),所以它只适合「不常变化的共享值」(主题/语言/用户/权限);优化靠「拆分 Context(值与方法分开)+ useMemo 包装 value + children 透传」;需要「选择器订阅」或高频更新就用状态管理库。

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

评论 抢沙发

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

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

微信扫一扫

支付宝扫一扫