React 学习站
React›核心›核心

Context 与跨层级通信

核心核心

一句话定义

Context 让一棵子树里的所有组件直接读取顶层提供的数据(createContext + <Provider> + useContext),不用逐层传 props——代价是消费组件会在 context 值变化时全部重渲染。

为什么重要

它解决了 prop drilling(穿层传递)这个结构性痛点;但它又是最容易被滥用的 API——「把整个应用状态都塞进一个大 Context」是反模式,会带来大面积重渲染与测试负担。学会「何时用、怎么切分」是核心目标。

前置知识

  • kp-004 prop drilling 是什么。
  • kp-008 触发重渲染的第三种方式(context 变化)。

核心概念

  • createContext(defaultValue):defaultValue 仅当树上没有 Provider 时生效(常用于让测试无需 Provider)。
  • Provider:把 value 提供给子树;value 变化 → 所有消费组件重渲染。
  • useContext(MyContext):在组件(或自定义 Hook)内读取最近 Provider 的 value。
  • 适用形态:低频变化、全局横切的「环境」:主题、当前语言、当前登录用户、路由实例。不是高频状态的车(高频用状态库,kp-021)。
  • 典型配套:Provider 里持有 state,value 里同时提供「状态 + setter」。

原理 / 机制

基础三件套:

jsxconst ThemeContext = createContext(null);

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  // value 引用稳定性重要:内联对象字面量会让每次渲染都"变化"
  const value = useMemo(() => ({ theme, setTheme }), [theme]);

  return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}

function useTheme() {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error('useTheme 必须在 <ThemeProvider> 内使用');
  return ctx;
}

// 任意深度的叶子直接用
function ThemeButton() {
  const { theme, setTheme } = useTheme();
  return <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>{theme}</button>;
}

重渲染链路:theme 变 → Provider 重渲染 → 新 value(useMemo 后引用稳定)→ 所有 useContext(ThemeContext) 的组件重渲染;未消费的中间层不受影响。若 value 不加 useMemo,{ theme, setTheme } 每次都是新对象 → 所有消费者在 Provider 的每次渲染都重渲染。

切分策略——「按变化频率与消费群体拆分 Provider」:

jsx// ✅ 拆分:主题变化只惊动主题消费者
<AuthProvider>        {/* 用户信息,登录才变 */}
  <ThemeContext.Provider>  {/* 主题,偶尔变 */}
    <RouterProvider>   {/* 路由,每次跳转变 */}
      <App />
    </RouterProvider>
  </ThemeContext.Provider>
</AuthProvider>

// ❌ 反模式:一个 AppStateContext 包罗万象 → 任何字段变化全树重渲染

直观类比

Context 是楼层公示栏:物业(Provider)贴公告,全楼住户(消费者)直接看,不需要前台逐层转达(prop drilling)。但每次公告换内容,整楼都会凑过来看一眼(重渲染)——所以重要但少变的「物业规章」(主题/语言)适合公示栏;高频变动的「电梯实时楼层」(每秒变的状态)贴公示栏只会让人群不停聚集,应该装个电梯显示屏(状态库/局部状态)。

实例 / 案例

「当前用户」的规范用法(读为主、写为事件):

jsxconst UserContext = createContext(null);

export function UserProvider({ children }) {
  const [user, setUser] = useState(null);

  useEffect(() => { fetchCurrentUser().then(setUser); }, []);

  const value = useMemo(
    () => ({ user, logout: () => { setUser(null); api.logout(); } }),
    [user]
  );
  return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}

export function useUser() { return useContext(UserContext); }

// 任意组件:
function Avatar() {
  const { user } = useUser();
  if (!user) return null;
  return <img src={user.avatarUrl} alt={user.name} />;
}

自带的 useUser()(而非散落各处的 useContext(UserContext))带来三件事:错误提示、未来可替换实现、语义化。

常见误区

  • 拿 Context 当全局状态库:高频变化(鼠标位置、滚动、倒计时)放进 context → 性能雪崩。React 官方明确:Context 用于「低频变化的环境数据」。
  • value 内联对象:value={{ a, b }} 每次渲染新引用,消费者全体陪跑;用 useMemo 或拆分 state。
  • 忘了 defaultValue 的语义:有 Provider 时它不生效;测试时「不包 Provider 得到默认值」是设计能力不是巧合。
  • 消费组件在 Provider 上游:读到的永远是 defaultValue(通常 null);把 Provider 放到尽可能高的正确位置。
  • 一个巨型 Context 对象:既难测试又难优化;按领域拆 Provider + 自定义 Hook。

自测题

  1. 写出「创建 → 提供三层 → 消费」的最小 Context 代码,并在消费 Hook 里做防护。
  2. 为什么 value 需要 useMemo?不加会发生什么级别的重渲染?
  3. 「鼠标坐标」适合放 Context 吗?给出判断依据和替代方案。
  4. 拆分 Provider 的两个原则是什么(变化频率 / 消费群体),各举一例。

与其他知识点的关系

  • 向前:替代 kp-004 的 drilling;实现上依赖 kp-005 state 与 kp-018 useMemo。
  • 向后:kp-019 会量化「context 变化引发的重渲染」在性能分析里的表现;kp-021 讲 Context 与 Zustand/Redux 的分工;kp-022 指出并发时代 context 更新同样受优先级调度。

延伸阅读

  • Passing Data Deeply with Context:<https://react.dev/learn/passing-data-deeply-with-context>
  • Scaling Up with Reducer and Context(官方组合教程):<https://react.dev/learn/scaling-up-with-reducer-and-context>