跳至正文
来两杯美式
返回

React 核心(7):Context API 与状态共享

By 来两杯美式
发布于

Context API 与状态共享

一句话定义:Context 是组件树里的”广播站”——上层播,下层听,中间层不用管。

一、为什么需要 Context?

1.1 Props 逐层传递的痛点

你已经知道 Props 是 React 数据流的基础——父传子,子传孙。但当数据需要穿过很多层组件时,问题来了:

// 顶层组件有主题数据
function App() {
  const theme = "dark";
  return <Layout theme={theme} />;
}
// 中间层根本不用 theme,但必须转发
function Layout({ theme }) {
  return <Sidebar theme={theme} />;
}
function Sidebar({ theme }) {
  return <Panel theme={theme} />;
}
// 终于到了真正需要 theme 的组件
function Panel({ theme }) {
  return <div className={theme}>内容</div>;
}

themeAppLayoutSidebarPanel,中间两层完全不用这个数据,却被迫接收并转发。这就是 Prop Drilling——像办公室传话,明明只有首尾两个人需要沟通,中间所有人都得当传声筒。

1.2 痛在哪

问题表现
中间层污染每个中间组件都要声明不用的 Props
改动牵连广数据结构变了,每一层都要改
代码噪音大量 {...props} 透传,难以追踪数据来源
类型维护TypeScript 下每层都要写类型定义

如果只有 2 层传递,Props 完全够用。3-4 层以上、且中间层不消费数据时,Context 才值得出场。

二、Context 是什么?

Context 提供了一种跨组件树传递数据的方式,不需要手动逐层传 Props。

类比:Context 像 WiFi 信号——路由器(Provider)在客厅广播,任何房间(消费者)都能直接连上,不需要”客厅传给走廊、走廊传给卧室”这样接力。

没有 Context(传话模式):
  App → Layout → Sidebar → Panel
  "暗色主题" → "帮我传一下" → "帮我传一下" → 终于收到了

有 Context(广播模式):
  App(广播)────────────────────→ Panel(直接收)
         Layout(不管)  Sidebar(不管)

Context 不是替代 Props,而是补充——大部分数据仍应通过 Props 传递,只有真正需要”全局共享”的数据才用 Context。

三、三个步骤:创建 → 提供 → 消费

3.1 createContext:创建广播频道

import { createContext } from "react";

// 创建一个 Context,参数是默认值
const ThemeContext = createContext("light");

createContext 返回一个对象,包含两个属性:

3.2 Provider:广播信号

function App() {
  const [theme, setTheme] = useState("dark");
  return (
    // 用 Provider 包裹子树,value 就是广播的数据
    <ThemeContext.Provider value={theme}>
      <Layout />
    </ThemeContext.Provider>
  );
}

Provider 的 value 属性就是你要共享的数据。被 Provider 包裹的所有后代组件都能读到。

3.3 useContext:接收信号

import { useContext } from "react";

function Panel() {
  const theme = useContext(ThemeContext); // 直接从 Context 读取,不需要 Props
  return <div className={theme}>当前主题:{theme}</div>;
}

useContext 接收一个 Context 对象,返回最近的 Provider 提供的 value


四、完整示例:主题切换

import { createContext, useContext, useState } from "react";

// 1. 创建 Context
const ThemeContext = createContext("light");

function App() {
  const [theme, setTheme] = useState("light");
  return (
    // 2. Provider 提供数据
    <ThemeContext.Provider value={theme}>
      <Toolbar />
      <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
        切换主题
      </button>
    </ThemeContext.Provider>
  );
}

function Toolbar() {
  return <ThemedButton />;
} // 中间层不需要 theme

function ThemedButton() {
  const theme = useContext(ThemeContext); // 3. useContext 消费数据
  return <button className={theme}>我是{theme}主题按钮</button>;
}

点击”切换主题” → theme 状态变化 → Provider 的 value 更新 → ThemedButton 自动重渲染。

三步口诀:createContext 建频道,Provider 发信号,useContext 收信号


五、工作机制详解

5.1 Provider 的 value 变化 → 所有消费者重渲染

这是 Context 最核心的机制:Provider 的 value 变了,所有调了 useContext(该Context) 的组件都会重渲染

<ThemeContext.Provider value={theme}>
  {/* value 变了,所有用 useContext(ThemeContext) 的组件都会重渲染 */}
  <DeepChild1 /> {/* 用了 → 重渲染 */}
  <DeepChild2 /> {/* 没用 → 不受影响 */}
  <DeepChild3 /> {/* 用了 → 重渲染 */}
</ThemeContext.Provider>

这也是 Context 性能问题的根源——后面第七节会详细讲。

5.2 查找最近的 Provider

useContext 不是”找最顶层的 Provider”,而是找组件树中最近的

const ThemeContext = createContext("light");

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Sidebar />
    </ThemeContext.Provider>
  );
}
function Sidebar() {
  return (
    // 这里的 Provider 离 Panel 更近,会覆盖外层的
    <ThemeContext.Provider value="blue">
      <Panel />
    </ThemeContext.Provider>
  );
}
function Panel() {
  const theme = useContext(ThemeContext);
  return <div>{theme}</div>; // 显示 "blue",不是 "dark"
}

就像手机连 WiFi——房间里有自己的路由器,就连最近的,不会绕远去连客厅的。

5.3 默认值什么时候生效?

createContext('light') 里的 'light' 是默认值,只在找不到任何 Provider 时才生效

const ThemeContext = createContext("light");

function App() {
  return <Panel />; // 没有 Provider 包裹
}
function Panel() {
  const theme = useContext(ThemeContext);
  return <div>{theme}</div>; // 显示 "light"(默认值)
}

实际开发中,默认值主要用于没有 Provider 的测试场景。正常使用时 Provider 总是存在的,默认值不会被用到。


六、实战模式

6.1 拆分 Provider 和自定义 Hook

直接导出 Context 对象有个问题:消费者需要知道 Context 名字,还要自己写 useContext。更好的做法是封装自定义 Hook

// ThemeContext.jsx
import { createContext, useContext, useState } from "react";

// Context 不导出,外部不直接用
const ThemeContext = createContext(undefined);

// Provider 组件:负责提供数据
export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("light");
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// 自定义 Hook:负责消费数据
export function useTheme() {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error("useTheme 必须在 ThemeProvider 内使用");
  }
  return context;
}

使用时:

// App.jsx —— 包 Provider
<ThemeProvider>
  <MyApp />
</ThemeProvider>;

// 消费者 —— 用自定义 Hook,干净
function ThemedButton() {
  const { theme, setTheme } = useTheme(); // 不用关心 Context 名字
  return (
    <button
      className={theme}
      onClick={() => setTheme(theme === "light" ? "dark" : "light")}
    >
      切换
    </button>
  );
}

这个模式的好处:① 消费者不用关心 Context 名字;② 忘了包 Provider 会报错而不是静默用默认值;③ 内部实现可以随时换(比如换成 Zustand),消费者代码不用改。

6.2 多个 Context 的组合使用

当有多个全局数据时,嵌套多个 Provider:

function App() {
  return (
    <ThemeProvider>
      <AuthProvider>
        <LocaleProvider>
          <MyApp />
        </LocaleProvider>
      </AuthProvider>
    </ThemeProvider>
  );
}

// 组件按需消费,互不影响
function Header() {
  const { theme } = useTheme(); // 只关心主题
  const { user } = useAuth(); // 只关心用户
  // 不关心 locale,不会因 locale 变化而重渲染
  return <header className={theme}>你好,{user.name}</header>;
}

每个 Context 独立触发重渲染——theme 变了不会导致只用了 useAuth 的组件重渲染。

6.3 Context + useState:简易状态管理

Context + useState 是最轻量的”全局状态管理”方案:

const UserContext = createContext(undefined);

export function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  const login = userData => setUser(userData);
  const logout = () => setUser(null);
  return (
    <UserContext.Provider value={{ user, login, logout }}>
      {children}
    </UserContext.Provider>
  );
}

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

适合:用户信息、权限、主题等状态简单、更新频率低的场景。

6.4 Context + useReducer:复杂状态

状态逻辑复杂(多子状态联动、复杂操作)时,用 useReducer 替代 useState

import { createContext, useContext, useReducer } from "react";

const CartContext = createContext(undefined);

// Reducer 定义状态更新逻辑
function cartReducer(state, action) {
  switch (action.type) {
    case "ADD_ITEM":
      return { ...state, items: [...state.items, action.payload] };
    case "REMOVE_ITEM":
      return {
        ...state,
        items: state.items.filter(i => i.id !== action.payload),
      };
    case "CLEAR":
      return { ...state, items: [] };
    default:
      return state;
  }
}

export function CartProvider({ children }) {
  const [state, dispatch] = useReducer(cartReducer, { items: [] });
  return (
    <CartContext.Provider value={{ state, dispatch }}>
      {children}
    </CartContext.Provider>
  );
}

export function useCart() {
  return useContext(CartContext);
}

消费端直接用 useCart() 拿到 statedispatch,调用 dispatch({ type: 'ADD_ITEM', payload: item }) 即可。

useReducer 的好处:① 状态更新逻辑集中在一处,不散落在各组件;② dispatch 引用稳定,不会因重渲染而变化——这能避免一些性能问题。


七、性能陷阱与优化

7.1 Provider value 引用不稳定

这是 Context 最常见的性能坑。看这段代码:

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("light");

  // ❌ 每次渲染都创建新对象 → 所有消费者都重渲染
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

问题:{ theme, setTheme } 每次渲染都是新对象,React 用 Object.is() 比较 value,发现”变了”就通知所有消费者重渲染——即使 theme 没变。

7.2 解决方案一:useMemo 包裹 value

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("light");
  // ✅ 只在 theme 变化时才创建新对象
  const value = useMemo(() => ({ theme, setTheme }), [theme]);
  // setTheme 引用稳定,不需要放进依赖
  return (
    <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
  );
}

7.3 解决方案二:把 state 本身作为 value

如果只需要共享数据,不需要共享 setter:

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("light");
  // ✅ theme 是字符串,值不变引用就不变
  return (
    <ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>
  );
}

7.4 解决方案三:拆分 Context 减少重渲染范围

当 Provider 的 value 包含多个独立数据时,可以把它们拆成多个 Context:

// ❌ 一个 Context 包含所有数据,任一变化都触发全部消费者重渲染
<AppContext.Provider value={{ user, theme, locale }}><App /></AppContext.Provider>

// ✅ 拆成多个 Context,各自独立
<UserContext.Provider value={user}>
  <ThemeContext.Provider value={theme}>
    <LocaleContext.Provider value={locale}>
      <App />
    </LocaleContext.Provider>
  </ThemeContext.Provider>
</UserContext.Provider>

更精细的做法——把数据和操作拆开:

// 数据 Context:theme 变了才重渲染
const ThemeStateContext = createContext("light");
// 操作 Context:setTheme 引用永远不变,消费者不会因操作重渲染
const ThemeDispatchContext = createContext(undefined);

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("light");
  return (
    <ThemeStateContext.Provider value={theme}>
      <ThemeDispatchContext.Provider value={setTheme}>
        {children}
      </ThemeDispatchContext.Provider>
    </ThemeStateContext.Provider>
  );
}

function useThemeState() {
  return useContext(ThemeStateContext);
}
function useThemeDispatch() {
  return useContext(ThemeDispatchContext);
} // 不会因 theme 变化而重渲染

拆分 Context 是 React 官方推荐的模式——把”读”和”写”分开,避免只需要”写”的组件因数据变化而无谓重渲染。


八、Context 的适用边界

8.1 适合用 Context 的场景

场景特点例子
主题/外观全局、低频变更暗色/亮色切换
国际化全局、几乎不变语言、时区
用户信息全局、登录时变用户名、头像、角色
权限控制全局、低频变更能否访问某功能

8.2 不适合的场景

共同特征:全局共享 + 更新频率低 + 消费者多

场景为什么不适合更好的选择
高频变更数据每次变更触发所有消费者重渲染状态库(Zustand)或 ref
细粒度更新只想更新列表中一项,却重渲染整个列表状态库 + selector
临时/局部状态只有两个组件需要共享Props 传递或状态提升
// ❌ 用 Context 共享鼠标位置——每帧都变,所有消费者疯狂重渲染
function App() {
  const [pos, setPos] = useState({ x: 0, y: 0 });
  useEffect(() => {
    const handler = e => setPos({ x: e.clientX, y: e.clientY });
    window.addEventListener("mousemove", handler); // 每秒几十次 → 灾难
    return () => window.removeEventListener("mousemove", handler);
  }, []);
  return (
    <MouseContext.Provider value={pos}>
      <ExpensiveTree />
    </MouseContext.Provider>
  );
}

8.3 什么时候该用状态库

Context + useState/useReducer 能覆盖大部分场景,但出现以下需求时,考虑 Zustand 或 Redux:

需求Context 能否胜任推荐
全局共享几个低频状态✅ 完全够用Context
需要细粒度订阅(只关心 state 的一部分)❌ 做不到Zustand selector
高频更新(拖拽、动画、实时数据)❌ 性能扛不住Zustand / ref
需要中间件、持久化、DevTools❌ 没有内置支持Redux / Zustand
状态逻辑极其复杂⚠️ 能做但维护难Redux(有规范模式)

简单判断:Provider value 只有 2-3 个字段、更新不频繁 → Context 够了。一旦你开始为了性能拆 Context、加 useMemo、搞 selector,说明该上状态库了。


九、Context 与 Props 的关系

Context 不是 Props 的替代品,而是补充

function UserCard({ userId }) {
  // userId 通过 Props 传入——这是对的
  const { getUser } = useUser(); // getUser 从 Context 获取——全局共享的
  const user = getUser(userId); // 用 Context 提供的方法 + Props 提供的参数
  return <div>{user.name}</div>;
}

原则

// ✅ 好的设计:Props 传具体数据,Context 提供全局能力
function ProductCard({ product }) {
  // product 是这个卡片特有的 → Props
  const { theme } = useTheme(); // theme 是全局的 → Context
  const { addToCart } = useCart(); // addToCart 是全局操作 → Context
  return (
    <div className={theme}>
      <h3>{product.name}</h3>
      <button onClick={() => addToCart(product)}>加入购物车</button>
    </div>
  );
}

// ❌ 滥用 Context:把所有数据都塞进 Context
<ProductContext.Provider value={product1}>
  <ProductCard /> {/* 卡片不知道自己展示哪个商品 */}
</ProductContext.Provider>;

经验法则:数据是”组件实例特有的”(如列表某一项)→ Props;是”整个应用共享的”(如当前用户、主题)→ Context。


十、一句话

Context 是组件树的广播站——createContext 建频道,Provider 发信号,useContext 收信号。适合低频全局数据,不适合高频细粒度更新。记住:Provider value 引用不稳定是性能杀手,拆分 Context 或 useMemo 是解药


分享这篇文章:
通过邮件分享这篇文章✓ 链接已复制
所属专题
React
第 7 / 17 篇
查看系列全部文章
  1. 01.React 核心(1):React 组件与函数的本质区别
  2. 02.React 核心(2):声明式与 React 的实现机制
  3. 03.React 核心(3):Props 完全指南
  4. 04.React 核心(4):useState 完全指南
  5. 05.React 核心(5):useEffect 完全指南
  6. 06.React 核心(6):useRef 完全指南
  7. 07.React 核心(7):Context API 与状态共享
  8. 08.React 核心(8):useCallback 与 useMemo
  9. 09.React 核心(9):Hooks 规则与闭包陷阱
  10. 10.React 核心(10):自定义 Hook 设计
  11. 11.React 核心(11):Key 与列表渲染
  12. 12.React 核心(12):表单与受控/非受控组件
  13. 13.React 核心(13):React 渲染机制深入——Fiber 与调度
  14. 14.React 核心(14):React 18 并发特性
  15. 15.React 核心(15):Suspense 机制详解
  16. 16.React 核心(16):TanStack Query 入门指南
  17. 17.React 核心(17):TanStack Query 缓存机制详解

上一篇
React 核心(8):useCallback 与 useMemo
下一篇
React 核心(6):useRef 完全指南