React Context API 完全指南:跨组件状态共享
React Context API 完全指南:跨组件状态共享
一、Introduction
在 React 应用中,数据通常沿着组件树从上往下传递(props drilling):顶层组件持有状态,通过 props 一层层往下传,即使中间某些组件根本不需要这个数据,也不得不当"传话筒"。Context API 解决这个问题:它允许你在组件树中创建"全局数据通道",让任何层级的组件直接访问和修改共享状态,而不需要通过中间组件传递。
Context 不是万能的——它会使得组件复用性降低,所以应该用在真正全局需要的数据上,比如:当前登录用户、主题、语言设置、全局通知等。本文详解 Context 的完整用法、创建模式、以及常见陷阱。
---
二、Context 的基本用法
#### 2.1 创建与使用 Context
import React, { createContext, useContext, useState } from 'react';
// 第一步:创建一个 Context 对象
// 可以传入默认值(不提供时使用)
const ThemeContext = createContext({
theme: 'light',
toggleTheme: () => {}
});
const UserContext = createContext(null);
// 第二步:创建 Provider 组件,包裹需要共享数据的组件树
function App() {
const [theme, setTheme] = useState('light');
const [user, setUser] = useState({ name: '游客', level: 0 });
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
// 使用 Provider 的 value 属性传递要共享的数据
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<UserContext.Provider value={user}>
<div
style={{
background: theme === 'light' ? '#fff' : '#222',
color: theme === 'light' ? '#333' : '#eee',
minHeight: '100vh',
padding: 20
}}
>
<h1>React Context 演示</h1>
<NavBar />
<ThemeToggle />
<UserInfo />
</div>
</UserContext.Provider>
</ThemeContext.Provider>
);
}
// 第三步:在需要的组件中用 useContext 获取数据
function NavBar() {
const { theme } = useContext(ThemeContext);
return (
<nav style={{ borderBottom: `1px solid ${theme === 'light' ? '#ddd' : '#555'}`, paddingBottom: 12, marginBottom: 16 }}>
导航栏 | 当前主题:{theme}
</nav>
);
}
function ThemeToggle() {
// 从 Context 获取数据和操作方法
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button
onClick={toggleTheme}
style={{
padding: '8px 16px',
background: theme === 'light' ? '#333' : '#f0f0f0',
color: theme === 'light' ? '#fff' : '#333',
border: 'none',
borderRadius: 4,
cursor: 'pointer'
}}
>
切换为{theme === 'light' ? '深色' : '浅色'}主题
</button>
);
}
function UserInfo() {
// 独立的 User Context,不受 Theme Context 影响
const user = useContext(UserContext);
return (
<div style={{ marginTop: 16 }}>
<p>当前用户:{user.name}(等级 {user.level})</p>
</div>
);
}
export default App;
---
三、Reducer + Context:重构复杂状态
#### 3.1 用 useReducer 管理复杂状态逻辑
当 Context 中的状态逻辑变得复杂时,应该把状态管理逻辑从组件中抽离出来,用 useReducer 配合 Context:
import React, { createContext, useContext, useReducer, useCallback } from 'react';
// ==================== 购物车状态管理 ====================
// 购物车状态初始值
const initialCartState = {
items: [], // [{ id, name, price, quantity }]
totalPrice: 0,
totalItems: 0
};
// reducer:接收当前状态和 action,返回新状态
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM': {
const existingIndex = state.items.findIndex(i => i.id === action.payload.id);
let newItems;
if (existingIndex >= 0) {
// 商品已存在,数量+1
newItems = state.items.map((item, idx) =>
idx === existingIndex
? { ...item, quantity: item.quantity + 1 }
: item
);
} else {
// 新商品
newItems = [...state.items, { ...action.payload, quantity: 1 }];
}
return {
items: newItems,
totalPrice: newItems.reduce((sum, i) => sum + i.price * i.quantity, 0),
totalItems: newItems.reduce((sum, i) => sum + i.quantity, 0)
};
}
case 'REMOVE_ITEM': {
const newItems = state.items.filter(i => i.id !== action.payload.id);
return {
items: newItems,
totalPrice: newItems.reduce((sum, i) => sum + i.price * i.quantity, 0),
totalItems: newItems.reduce((sum, i) => sum + i.quantity, 0)
};
}
case 'UPDATE_QUANTITY': {
const newItems = state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: Math.max(1, action.payload.quantity) }
: item
);
return {
items: newItems,
totalPrice: newItems.reduce((sum, i) => sum + i.price * i.quantity, 0),
totalItems: newItems.reduce((sum, i) => sum + i.quantity, 0)
};
}
case 'CLEAR_CART':
return initialCartState;
default:
return state;
}
}
// ==================== Context 定义 ====================
const CartContext = createContext(null);
// ==================== Provider 组件 ====================
const PRODUCTS = [
{ id: 1, name: 'React 进阶', price: 99 },
{ id: 2, name: 'TypeScript 实战', price: 79 },
{ id: 3, name: 'Node.js 深入', price: 129 },
{ id: 4, name: 'GraphQL 指南', price: 59 },
];
function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, initialCartState);
// 用 useCallback 包裹,确保 dispatch 引用稳定
const addItem = useCallback((product) => {
dispatch({ type: 'ADD_ITEM', payload: product });
}, []);
const removeItem = useCallback((id) => {
dispatch({ type: 'REMOVE_ITEM', payload: { id } });
}, []);
const updateQuantity = useCallback((id, quantity) => {
dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } });
}, []);
const clearCart = useCallback(() => {
dispatch({ type: 'CLEAR_CART' });
}, []);
const value = {
cart: state,
addItem,
removeItem,
updateQuantity,
clearCart,
products: PRODUCTS
};
return (
<CartContext.Provider value={value}>
{children}
</CartContext.Provider>
);
}
// ==================== 消费组件 ====================
function ProductList() {
const { products, addItem } = useContext(CartContext);
return (
<div>
<h3>商品列表</h3>
<ul>
{products.map(product => (
<li key={product.id} style={{ marginBottom: 8 }}>
{product.name} - ¥{product.price}
<button
onClick={() => addItem(product)}
style={{ marginLeft: 12 }}
>
加入购物车
</button>
</li>
))}
</ul>
</div>
);
}
function CartSummary() {
const { cart, removeItem, updateQuantity, clearCart } = useContext(CartContext);
if (cart.items.length === 0) {
return <p>购物车为空</p>;
}
return (
<div>
<h3>购物车({cart.totalItems} 件商品)</h3>
<ul>
{cart.items.map(item => (
<li key={item.id} style={{ marginBottom: 8 }}>
{item.name} - ¥{item.price} × {item.quantity}
<button onClick={() => updateQuantity(item.id, item.quantity - 1)} style={{ marginLeft: 8 }}>−</button>
<button onClick={() => updateQuantity(item.id, item.quantity + 1)}>+</button>
<button onClick={() => removeItem(item.id)} style={{ marginLeft: 8, color: 'red' }}>删除</button>
</li>
))}
</ul>
<p><strong>总价:¥{cart.totalPrice}</strong></p>
<button onClick={clearCart}>清空购物车</button>
</div>
);
}
function ShoppingCartApp() {
return (
<CartProvider>
<div style={{ display: 'flex', gap: 40 }}>
<ProductList />
<CartSummary />
</div>
</CartProvider>
);
}
export default ShoppingCartApp;
---
四、运行效果说明
ThemeContext 示例:点击"切换为深色主题"按钮,NavBar、ThemeToggle、UserInfo 的背景色和文字颜色全部改变,因为它们都从 ThemeContext 读取主题数据。UserInfo 组件同时也从 UserContext 读取用户数据,两者独立不干扰。
ShoppingCartApp 示例:点击商品旁的"加入购物车",购物车实时更新商品数量和总价;点击 +/- 按钮调整数量;点击"删除"移除商品;点击"清空购物车"重置整个购物车状态。所有状态由 useReducer 统一管理,逻辑清晰可预测。
---
五、常见问题(FAQ)
#### Q1:Context 的 value 变化时,所有消费组件都会重新渲染吗?
是的。 这是 Context 最大的性能问题——Provider 下的所有消费组件,只要引用了那个 Context,不管用的是不是变化的部分,都会在 Provider 的 value 变化时重新渲染。解决方案有三种:
1. 拆分 Context:把经常变化的状态和很少变化的状态拆分到不同的 Context 中
2. 用 memo 包装消费组件:减少不必要的渲染
3. 使用第三方状态管理库(Zustand、Jotai)代替 Context 处理频繁变化的状态
// 不好:一个 Context 包含所有数据,任何变化都导致全部重新渲染
const AppContext = createContext({ theme, user, notifications, cart });
// 好:拆分为多个 Context,变化隔离
const ThemeContext = createContext({ theme });
const UserContext = createContext({ user });
const NotificationContext = createContext({ notifications });
const CartContext = createContext({ cart });
#### Q2:可以在 Context Provider 外部使用 useContext 吗?
可以,但会使用默认值。 createContext() 的参数是默认值,仅在 Provider 树外部消费 Context 时使用(通常用于独立测试组件)。实际应用中,在 Provider 树内使用时默认值被忽略。
const ThemeContext = createContext('light'); // 默认值
// 在 Provider 外使用时,返回默认值 'light'
function StandaloneButton() {
const theme = useContext(ThemeContext); // theme === 'light'
return <button style={{ background: theme === 'light' ? '#fff' : '#333' }}>按钮</button>;
}
#### Q3:Context 的 Provider 可以嵌套使用吗?
可以。 同一个 Context 类型可以多次 Provider,后续 Provider 会覆盖前面的值:
<ThemeContext.Provider value={{ theme: 'light' }}>
<div>这里用浅色主题</div>
<ThemeContext.Provider value={{ theme: 'dark' }}>
<div>这里用深色主题,覆盖了外层</div>
</ThemeContext.Provider>
</ThemeContext.Provider>
---
六、延伸阅读
- [React Hooks 完全指南:useState 与 useEffect 核心用法]()
- [React 自定义 Hooks 实战:逻辑复用与模块化]()
- [React 状态管理演进:从 useState 到 Redux]()
- [React useCallback 与 useMemo 性能优化完全指南]()
---