别再 Prop Drilling 了!一文彻底搞懂 React 组件通信的 5 种方案
🎯 本文从实际项目出发,带你掌握 Props、Context、useReducer 等 React 组件通信方案,附赠完整购物车实战 demo
前言
在 React 的世界里,一切皆组件。但当组件树变得复杂时,一个现实的问题摆在我们面前------组件之间如何高效通信?
想象一下这个场景:
graph TD
A[App] --> B[Header]
A --> C[Main]
A --> D[Footer]
B --> E[Logo]
B --> F[Nav]
F --> G[UserAvatar]
C --> H[Dashboard]
H --> I[Profile]
style G fill:#ff9999
style I fill:#ff9999
UserAvatar 和 Profile 都需要用户数据,但它们相隔十万八千里。如果一层层传 props,代码会变成什么样?
jsx
<App user={user}>
<Header user={user}>
<Nav user={user}>
<UserAvatar user={user} /> {/* 第4层才用到 */}
</Nav>
</Header>
<Main user={user}>
<Dashboard user={user}>
<Profile user={user} /> {/* 第4层才用到 */}
</Dashboard>
</Main>
</App>
这就是传说中的 "Prop Drilling"(props 钻孔)💀 中间组件被迫传递自己根本不用的数据,代码臃肿、维护困难。
今天我们来系统解决这个问题!
一、组件通信的五种场景
graph LR
A[组件通信] --> B[父子通信]
A --> C[兄弟通信]
A --> D[跨层级通信]
A --> E[全局状态]
B --> B1[Props]
B --> B2[回调函数]
C --> C1[状态提升]
D --> D1[useContext]
E --> E1[useReducer + Context]
E --> E2[Redux / Zustand]
| 场景 | 关系 | 推荐方案 | 复杂度 |
|---|---|---|---|
| 父 → 子 | 直接嵌套 | Props | ⭐ |
| 子 → 父 | 直接嵌套 | 回调函数 | ⭐ |
| 兄弟 | 同一父级 | 状态提升 | ⭐⭐ |
| 跨层级 | 任意关系 | useContext | ⭐⭐ |
| 全局复杂状态 | 任意关系 | useContext + useReducer | ⭐⭐⭐ |
二、父子通信:Props(基础中的基础)
这是最基础的通信方式,单向数据流是 React 的核心理念。
父传子:Props
jsx
import { useState } from 'react';
// 父组件
function Parent() {
const message = "Hello from Parent";
return <Child message={message} />;
}
// 子组件
function Child({ message }) {
return <div>{message}</div>;
}
子传父:回调函数
jsx
import { useState } from 'react';
// 父组件
function Parent() {
const [data, setData] = useState('');
return <Child onSubmit={(val) => setData(val)} />;
}
// 子组件
function Child({ onSubmit }) {
return <button onClick={() => onSubmit('来自子组件')}>发送</button>;
}
兄弟通信:状态提升
jsx
import { useState } from 'react';
function Parent() {
const [shared, setShared] = useState('');
return (
<>
<BrotherA onSend={setShared} />
<BrotherB data={shared} />
</>
);
}
💡 核心原则:状态找最近的公共祖先,通过 props 向下分发
三、跨层级通信:useContext(本文重点)
当组件层级变深,props 逐层传递就变得不可维护。这时候 useContext 闪亮登场!
核心概念
graph LR
A[createContext] -->|创建通道| B[Context 对象]
B -->|Provider 包裹| C[组件树]
C -->|useContext 消费| D[任意子组件]
style A fill:#4CAF50,color:white
style B fill:#2196F3,color:white
style C fill:#FF9800,color:white
style D fill:#9C27B0,color:white
- Context = 创建共享数据的"通道"
- Provider = 数据的"发射塔"
- useContext = 数据的"接收器"
三步走战略
Step 1:创建 Context
jsx
// ThemeContext.jsx
import { createContext } from 'react';
// 创建上下文,参数是默认值
export const ThemeContext = createContext('light');
Step 2:用 Provider 包裹组件树
jsx
// App.jsx
import { useState } from 'react';
import { ThemeContext } from './ThemeContext';
import Page from './components/Page';
function App() {
const [theme, setTheme] = useState('light');
return (
// Provider 提供数据,value 属性传递具体值
<ThemeContext.Provider value={theme}>
<Page />
<button onClick={() => setTheme('dark')}>切换主题</button>
</ThemeContext.Provider>
);
}
Step 3:任意子组件消费 Context
jsx
// components/Child.jsx
import { useContext } from 'react';
import { ThemeContext } from '../ThemeContext';
function Child() {
const theme = useContext(ThemeContext); // 直接获取,无需 props!
return (
<button className={theme}>
按钮 - 当前主题: {theme}
</button>
);
}
看看完整链路
graph TD
A["App (Provider value='light')"] --> B[Page]
B --> C[Child]
C -->|useContext 直接获取| A
style A fill:#4CAF50,color:white
style C fill:#2196F3,color:white
对比一下 Props 方式:
graph TD
subgraph "❌ Prop Drilling"
A1[App] -->|theme| B1[Page]
B1 -->|theme| C1[Child]
end
subgraph "✅ Context"
A2[App Provider] --> B2[Page]
A2 --> C2[Child]
C2 -.->|useContext| A2
end
jsx
// ❌ Props 逐层传递 (Prop Drilling)
<App>
<Page theme={theme}>
<Child theme={theme} />
</Page>
</App>
// ✅ Context 跨越传递
<ThemeContext.Provider value={theme}>
<Page /> {/* 不需要传 theme */}
<Child /> {/* 直接 useContext 获取 */}
</ThemeContext.Provider>
四、自定义 Hooks:优雅封装 Context
直接在组件里 useContext 没问题,但更好的实践是封装成自定义 Hook。
为什么封装?
- 语义化 :
useTheme()比useContext(ThemeContext)更清晰 - 可维护:Context 来源变更只需改一处
- 可复用:多个组件统一调用方式
- 类型安全:TypeScript 项目中可以获得更好的类型推导
封装示例
jsx
// hooks/useTheme.js
import { useContext } from 'react';
import { ThemeContext } from '../contexts/ThemeContext';
// 自定义 Hook:封装 useContext 调用
export function useTheme() {
const context = useContext(ThemeContext);
// 可选:添加错误边界
if (context === undefined) {
throw new Error('useTheme 必须在 ThemeProvider 内部使用');
}
return context;
}
在组件中使用
jsx
// Page.jsx
import { useTheme } from '../hooks/useTheme';
const Page = () => {
const theme = useTheme(); // 一行搞定!
return (
<>
当前主题: {theme}
<Child />
</>
);
};
推荐的目录结构
bash
src/
├── contexts/
│ ├── ThemeContext.jsx # 主题 Context
│ └── AuthContext.jsx # 认证 Context
├── hooks/
│ ├── useTheme.js # 主题 Hook
│ └── useAuth.js # 认证 Hook
├── components/
│ ├── Page.jsx
│ └── Child.jsx
└── App.jsx
💡 架构提示:hooks 目录属于架构层,封装响应式逻辑和副作用
五、实战:购物车状态管理(完整 Demo)
理论讲完了,来个真实业务场景------购物车。
完整代码实现
jsx
// contexts/CartContext.jsx
import { createContext, useContext, useReducer } from 'react';
// 1. 创建 Context
const CartContext = createContext();
// 2. 定义 Reducer
const cartReducer = (state, action) => {
switch (action.type) {
case 'ADD_ITEM': {
const existing = state.items.find(item => item.id === action.payload.id);
if (existing) {
return {
...state,
items: state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: item.quantity + 1 }
: item
),
};
}
return {
...state,
items: [...state.items, { ...action.payload, quantity: 1 }],
};
}
case 'REMOVE_ITEM':
return {
...state,
items: state.items.filter(item => item.id !== action.payload),
};
case 'UPDATE_QUANTITY':
return {
...state,
items: state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: action.payload.quantity }
: item
),
};
case 'CLEAR_CART':
return { ...state, items: [] };
default:
return state;
}
};
// 3. Provider 组件
export function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, { items: [] });
// 派生状态:计算总价
const total = state.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
return (
<CartContext.Provider value={{ ...state, total, dispatch }}>
{children}
</CartContext.Provider>
);
}
// 4. 自定义 Hook
export function useCart() {
const context = useContext(CartContext);
if (!context) {
throw new Error('useCart 必须在 CartProvider 内部使用');
}
return context;
}
在组件中使用
jsx
// components/ProductCard.jsx
import { useCart } from '../contexts/CartContext';
function ProductCard({ product }) {
const { dispatch } = useCart();
return (
<div className="product-card">
<h3>{product.name}</h3>
<p>¥{product.price}</p>
<button onClick={() => dispatch({ type: 'ADD_ITEM', payload: product })}>
加入购物车
</button>
</div>
);
}
jsx
// components/Cart.jsx
import { useCart } from '../contexts/CartContext';
function Cart() {
const { items, total, dispatch } = useCart();
return (
<div className="cart">
<h2>购物车</h2>
{items.length === 0 ? (
<p>购物车是空的</p>
) : (
<>
{items.map(item => (
<div key={item.id} className="cart-item">
<span>{item.name}</span>
<span>¥{item.price} × {item.quantity}</span>
<button onClick={() => dispatch({ type: 'REMOVE_ITEM', payload: item.id })}>
删除
</button>
</div>
))}
<div className="cart-total">总计: ¥{total}</div>
<button onClick={() => dispatch({ type: 'CLEAR_CART' })}>
清空购物车
</button>
</>
)}
</div>
);
}
使用 Provider 包裹应用
jsx
// App.jsx
import { CartProvider } from './contexts/CartContext';
import ProductCard from './components/ProductCard';
import Cart from './components/Cart';
function App() {
const products = [
{ id: 1, name: 'React 教程', price: 99 },
{ id: 2, name: 'TypeScript 实战', price: 129 },
];
return (
<CartProvider>
<div className="app">
<div className="products">
{products.map(p => <ProductCard key={p.id} product={p} />)}
</div>
<Cart />
</div>
</CartProvider>
);
}
六、Context 性能陷阱与优化方案
:::warning 性能警告 Context 值变化会导致所有消费者重渲染,即使消费者没有使用变化的那部分数据! :::
问题演示
jsx
// ❌ 问题代码
function App() {
const [theme, setTheme] = useState('light');
const [user, setUser] = useState({ name: 'Alice' });
return (
<ThemeContext.Provider value={{ theme, user }}>
<Header /> {/* theme 变化 → Header 重渲染 */}
<Main /> {/* theme 变化 → Main 重渲染 */}
</ThemeContext.Provider>
);
}
解决方案 1:拆分 Context
jsx
// ✅ 拆分 Context
export const ThemeContext = createContext('light');
export const UserContext = createContext(null);
function App() {
const [theme, setTheme] = useState('light');
const [user, setUser] = useState({ name: 'Alice' });
return (
<ThemeContext.Provider value={theme}>
<UserContext.Provider value={user}>
<Header /> {/* 只订阅需要的 Context */}
<Main />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
解决方案 2:useMemo 优化
jsx
// ✅ 使用 useMemo 避免不必要的重渲染
function App() {
const [theme, setTheme] = useState('light');
const [user, setUser] = useState({ name: 'Alice' });
const themeValue = useMemo(() => ({ theme }), [theme]);
const userValue = useMemo(() => ({ user }), [user]);
return (
<ThemeContext.Provider value={themeValue}>
<UserContext.Provider value={userValue}>
<Header />
<Main />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
解决方案 3:React.memo + 拆分组件
jsx
// ✅ 使用 React.memo 包裹消费者
const Header = React.memo(function Header() {
const { theme } = useContext(ThemeContext);
return <header className={theme}>...</header>;
});
性能对比表
| 方案 | 重渲染范围 | 适用场景 |
|---|---|---|
| 单一 Context | 所有消费者 | 简单应用 |
| 拆分 Context | 按需订阅 | 中大型应用 |
| useMemo 优化 | 减少引用变化 | 值对象稳定 |
| React.memo | 跳过无关渲染 | 纯展示组件 |
七、useContext vs Redux vs Zustand 怎么选?
| 特性 | useContext | Redux | Zustand |
|---|---|---|---|
| 学习成本 | ⭐ 低 | ⭐⭐⭐ 高 | ⭐⭐ 中 |
| 样板代码 | 少 | 多 | 极少 |
| 性能 | 一般 | 优秀 | 优秀 |
| DevTools | 无 | 强大 | 有 |
| 中间件 | 无 | 丰富 | 支持 |
| 适用规模 | 小型 | 大型 | 中小型 |
选型决策树
graph TD
A[需要共享状态?] --> B{组件关系?}
B -->|父子| C[Props]
B -->|兄弟| D[状态提升]
B -->|跨层级| E{状态复杂度?}
E -->|简单| F[useContext]
E -->|中等| G[useContext + useReducer]
E -->|复杂| H{项目规模?}
H -->|中小型| I[Zustand]
H -->|大型| J[Redux Toolkit]
style C fill:#4CAF50,color:white
style F fill:#2196F3,color:white
style G fill:#2196F3,color:white
style I fill:#FF9800,color:white
style J fill:#f44336,color:white
八、最佳实践总结
✅ 推荐做法
- 就近原则:Provider 放在需要共享数据的最近公共祖先
- 封装 Hook :用
useXxx()封装useContext调用,添加错误边界 - 拆分 Context:不同维度的数据用不同的 Context
- 默认值 :
createContext传入合理的默认值 - TypeScript:为 Context 添加类型定义
❌ 避免踩坑
- 不要滥用:简单的父子通信用 Props 就够了
- 性能注意:Context 值变化会导致所有消费者重渲染
- 避免嵌套地狱:超过 3 层 Provider 考虑 Zustand
- 避免循环依赖:Context 和 Hook 文件要分离
TypeScript 类型定义示例
tsx
// types/cart.ts
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
interface CartState {
items: CartItem[];
total: number;
}
type CartAction =
| { type: 'ADD_ITEM'; payload: Omit<CartItem, 'quantity'> }
| { type: 'REMOVE_ITEM'; payload: number }
| { type: 'UPDATE_QUANTITY'; payload: { id: number; quantity: number } }
| { type: 'CLEAR_CART' };
总结
graph LR
A[简单] -->|Props| B[父子]
B -->|状态提升| C[兄弟]
C -->|useContext| D[跨层级]
D -->|useReducer| E[复杂状态]
E -->|Zustand/Redux| F[全局状态]
style A fill:#4CAF50,color:white
style D fill:#2196F3,color:white
style F fill:#f44336,color:white
| 方案 | 适用场景 | 复杂度 |
|---|---|---|
| Props | 父子通信 | ⭐ |
| 回调函数 | 子向父通信 | ⭐ |
| 状态提升 | 兄弟通信 | ⭐⭐ |
| useContext | 跨层级通信 | ⭐⭐ |
| useContext + useReducer | 复杂状态 | ⭐⭐⭐ |
| Zustand / Redux | 全局复杂状态 | ⭐⭐⭐ |
React 的组件通信从简单到复杂,选择合适的方案才是关键:
- 简单场景别过度设计 --- Props 永远是首选
- 复杂场景别硬撑 Props --- 该用 Context 就用
- 封装 Hook 提升可维护性 --- 代码更干净
- 性能优化要趁早 --- 拆分 Context + React.memo
📚 参考资料
- React 官方文档 - Passing Data Deeply with Context
- React 官方文档 - Reusing Logic with Custom Hooks
- Zustand 官方文档
🎯 本文代码:基于 context-demo 学习项目整理,完整购物车 demo 可直接运行
如果这篇文章对你有帮助,欢迎点赞 + 收藏,祝大家面试顺利拿 Offer!🎉
下期预告:深入理解 React Hooks 原理------高频面试题全解析