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>;
}
theme 从 App → Layout → Sidebar → Panel,中间两层完全不用这个数据,却被迫接收并转发。这就是 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 返回一个对象,包含两个属性:
Provider——广播信号的组件- 本身——传给
useContext用来接收信号
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() 拿到 state 和 dispatch,调用 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:组件的”环境”——隐式依赖的全局数据,调用者不需要显式传递
// ✅ 好的设计: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 是解药。