2026-04-29 22:19:51 +08:00
|
|
|
|
---
|
2026-04-29 23:36:32 +08:00
|
|
|
|
tags: [React, Component Communication, Props, Context, State Lifting, Frontend]
|
2026-04-29 22:19:51 +08:00
|
|
|
|
create time: 2026-04-29 22:10
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
# 组件通信模式
|
|
|
|
|
|
|
|
|
|
|
|
## 概述
|
|
|
|
|
|
|
2026-04-29 23:36:32 +08:00
|
|
|
|
React 的核心理念是 **"单向数据流"** ——数据从父组件流向子组件,事件从子组件冒泡回父组件。但在真实项目中,组件层级可以很深、结构可以很散,跨组件共享状态成为日常需求。
|
|
|
|
|
|
|
|
|
|
|
|
本文档梳理 React 中所有主流的组件通信方式,从最基础的 Props 到状态管理库,帮助你根据实际场景选择最合适方案。
|
|
|
|
|
|
|
|
|
|
|
|
> [!question] 思考题
|
|
|
|
|
|
> 假设你有一个 6 层深的组件树,最顶层需要把 `theme` 传给第 6 层的按钮组件。你会怎么做?逐层透传 Props 似乎笨拙,但引入 Context 又可能引发不必要的重渲染——你怎么权衡?
|
2026-04-29 22:19:51 +08:00
|
|
|
|
|
|
|
|
|
|
## 通信方向全景图
|
|
|
|
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
|
|
graph TD
|
|
|
|
|
|
A["父 → 子"] --> B["Props(最基础)"]
|
|
|
|
|
|
A --> C["Context Provider(跨层级)"]
|
|
|
|
|
|
A --> D["状态管理库(全局共享)"]
|
2026-04-29 23:36:32 +08:00
|
|
|
|
|
2026-04-29 22:19:51 +08:00
|
|
|
|
E["子 → 父"] --> F["回调 Prop(事件驱动)"]
|
2026-04-29 23:36:32 +08:00
|
|
|
|
E --> G["Refs(imperative)"]
|
|
|
|
|
|
|
2026-04-29 22:19:51 +08:00
|
|
|
|
H["兄弟组件"] --> I["状态提升到共同父组件"]
|
|
|
|
|
|
H --> J["通过共同祖先通信"]
|
|
|
|
|
|
H --> K["状态管理库 / 发布订阅"]
|
2026-04-29 23:36:32 +08:00
|
|
|
|
|
2026-04-29 22:19:51 +08:00
|
|
|
|
style B fill:#4FC08D,color:#fff
|
|
|
|
|
|
style C fill:#F5A87D,color:#000
|
|
|
|
|
|
style F fill:#61DAFB,color:#000
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## Props Drill(属性逐层透传)
|
|
|
|
|
|
|
|
|
|
|
|
### 问题场景
|
|
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
|
function App() {
|
|
|
|
|
|
const user = getUser();
|
2026-04-29 23:36:32 +08:00
|
|
|
|
return <Header user={user} />;
|
|
|
|
|
|
// Header 不需要 user,但深层的 Avatar 需要!
|
2026-04-29 22:19:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function Header({ user }) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<nav>
|
2026-04-29 23:36:32 +08:00
|
|
|
|
<Logo />
|
|
|
|
|
|
{/* ❌ 无需 user,却被迫接收 */}
|
|
|
|
|
|
<Avatar user={user} />
|
2026-04-29 22:19:51 +08:00
|
|
|
|
</nav>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-04-29 23:36:32 +08:00
|
|
|
|
> [!tip] 何时该容忍 Props Drill?
|
|
|
|
|
|
> 传递 2-3 层 Props 是完全合理的,甚至值得鼓励——它让数据流向清晰可见。只有超过 4 层时才考虑替代方案。
|
|
|
|
|
|
|
2026-04-29 22:19:51 +08:00
|
|
|
|
### 解决方案对比
|
|
|
|
|
|
|
|
|
|
|
|
| 方法 | 适合场景 | 额外依赖 |
|
|
|
|
|
|
|------|----------|---------|
|
2026-04-29 23:36:32 +08:00
|
|
|
|
| **Context** | 少量深层传递(主题、用户信息、语言设置) | 无 |
|
|
|
|
|
|
| **自定义 Hook + Props 重组织** | 中等深度(3-4 层),只需中间层不暴露 | 无 |
|
|
|
|
|
|
| **状态管理库** | 频繁变化、多组件共享 | Zustand / Redux Toolkit |
|
|
|
|
|
|
| **Render Props / HOC** | 复用逻辑而非单纯传值 | 无 |
|
2026-04-29 22:19:51 +08:00
|
|
|
|
|
|
|
|
|
|
## Callback Prop(子 → 父)
|
|
|
|
|
|
|
2026-04-29 23:36:32 +08:00
|
|
|
|
这是 React 中最基础也最重要的反向通信方式:**父组件传递一个函数作为 Prop,子组件在适当时机调用它**。
|
|
|
|
|
|
|
2026-04-29 22:19:51 +08:00
|
|
|
|
```tsx
|
|
|
|
|
|
interface FormProps {
|
|
|
|
|
|
onSubmit: (data: FormValues) => Promise<void>;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function LoginForm({ onSubmit }: FormProps) {
|
|
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
const data = collectFormData();
|
2026-04-29 23:36:32 +08:00
|
|
|
|
onSubmit(data); // ✅ 通知父组件
|
2026-04-29 22:19:51 +08:00
|
|
|
|
};
|
2026-04-29 23:36:32 +08:00
|
|
|
|
|
2026-04-29 22:19:51 +08:00
|
|
|
|
return <form onSubmit={handleSubmit}><Button type="submit">登录</Button></form>;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 父组件
|
|
|
|
|
|
function App() {
|
|
|
|
|
|
const handleLogin = async (data: FormValues) => {
|
|
|
|
|
|
await api.login(data);
|
|
|
|
|
|
navigate("/dashboard");
|
|
|
|
|
|
};
|
2026-04-29 23:36:32 +08:00
|
|
|
|
|
2026-04-29 22:19:51 +08:00
|
|
|
|
return <LoginForm onSubmit={handleLogin} />;
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-04-29 23:36:32 +08:00
|
|
|
|
> [!note] 为什么不用 children 做通信?
|
|
|
|
|
|
> children 是 UI 内容插槽,不是通信机制。回调 Prop 保持关注点分离——子组件只关心"什么时候触发",父组件决定"触发后做什么"。
|
|
|
|
|
|
|
|
|
|
|
|
> [!important] 性能提示:使用 useCallback
|
|
|
|
|
|
> 如果子组件用 `React.memo` 包裹,每次父组件重新渲染都会创建新的函数引用,导致子组件无效重渲染。用 `useCallback` 缓存回调函数:
|
|
|
|
|
|
> ```tsx
|
|
|
|
|
|
> const handleLogin = useCallback(async (data: FormValues) => { ... }, []);
|
|
|
|
|
|
> ```
|
2026-04-29 22:19:51 +08:00
|
|
|
|
|
|
|
|
|
|
## Context 跨层级通信
|
|
|
|
|
|
|
2026-04-29 23:36:32 +08:00
|
|
|
|
当 Props 穿透超过 3-4 层时,Context 是最轻量的替代方案。**它的本质是一个"隐式 Prop"——Provider 上方的任意后代都可以直接消费,无需经过中间组件。**
|
|
|
|
|
|
|
2026-04-29 22:19:51 +08:00
|
|
|
|
```tsx
|
|
|
|
|
|
const UserContext = createContext<User | null>(null);
|
|
|
|
|
|
|
2026-04-29 23:36:32 +08:00
|
|
|
|
function UserProfileProvider({ children }: { children: React.ReactNode }) {
|
2026-04-29 22:19:51 +08:00
|
|
|
|
const user = useDatabaseUser(userId);
|
2026-04-29 23:36:32 +08:00
|
|
|
|
|
2026-04-29 22:19:51 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<UserContext.Provider value={user}>
|
2026-04-29 23:36:32 +08:00
|
|
|
|
{children} {/* Avatar、Settings 任意深度都可消费 */}
|
2026-04-29 22:19:51 +08:00
|
|
|
|
</UserContext.Provider>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function OrderHistory() {
|
2026-04-29 23:36:32 +08:00
|
|
|
|
const user = useContext(UserContext); // ✅ 直接获取,跳过中间层
|
|
|
|
|
|
return <div>{user?.orders?.map(order => <OrderItem key={order.id} order={order} />)}</div>;
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### Context 性能陷阱与最佳实践
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
⚠️ 核心问题:
|
|
|
|
|
|
Context value 改变 → Provider 下所有消费者重渲染,无论是否真的使用了这个值
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**陷阱一:value 对象每次都是新引用**
|
|
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
|
// ❌ 每次渲染都创建新对象,所有消费者必重渲染
|
|
|
|
|
|
<UserContext.Provider value={{ user, theme }}>
|
|
|
|
|
|
{children}
|
|
|
|
|
|
</UserContext.Provider>
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**陷阱二:单个大 Context 包含多个独立状态**
|
|
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
|
// ❌ settings 变了,即使用户信息模块的组件也会重渲染
|
|
|
|
|
|
<UserAndSettingsContext.Provider value={{ user, settings, locale }}>
|
|
|
|
|
|
{children}
|
|
|
|
|
|
</UserAndSettingsContext.Provider>
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**最佳实践:拆分成小 Context + useReducer 稳定引用**
|
|
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
|
// ✅ 拆分 Context,粒度更细
|
|
|
|
|
|
const UserContext = createContext<User | null>(null);
|
|
|
|
|
|
const SettingsContext = createContext<Settings>({ theme: "light" });
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 用 useReducer 返回稳定的 { state, dispatch } 对象
|
|
|
|
|
|
function SettingsProvider({ children }: { children: React.ReactNode }) {
|
|
|
|
|
|
const [settings, dispatch] = useReducer(settingsReducer, initialSettings);
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<SettingsContext.Provider value={{ settings, dispatch }}>
|
|
|
|
|
|
{children}
|
|
|
|
|
|
</SettingsContext.Provider>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 消费者只取需要的 slice
|
|
|
|
|
|
function ThemeToggle() {
|
|
|
|
|
|
const { settings, dispatch } = useContext(SettingsContext);
|
|
|
|
|
|
return (
|
|
|
|
|
|
<button onClick={() => dispatch({ type: "TOGGLE_THEME" })}>
|
|
|
|
|
|
{settings.theme === "light" ? "☀️" : "🌙"}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 兄弟组件通信
|
|
|
|
|
|
|
|
|
|
|
|
两个没有祖先后代关系的组件如何共享数据?答案是:**将状态提升到它们的共同父组件中**。
|
|
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
|
type ContactId = string;
|
|
|
|
|
|
|
|
|
|
|
|
function ChatApp() {
|
|
|
|
|
|
const [contacts, setContacts] = useState<Contact[]>([]);
|
|
|
|
|
|
const [selectedId, setSelectedId] = useState<ContactId | null>(null);
|
|
|
|
|
|
const [messages, setMessages] = useState<Message[]>([]);
|
|
|
|
|
|
|
|
|
|
|
|
const selectedContact = useMemo(
|
|
|
|
|
|
() => contacts.find(c => c.id === selectedId),
|
|
|
|
|
|
[contacts, selectedId]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="chat-layout">
|
|
|
|
|
|
{/* 左侧:联系人列表 */}
|
|
|
|
|
|
<ContactList
|
|
|
|
|
|
contacts={contacts}
|
|
|
|
|
|
selectedId={selectedId}
|
|
|
|
|
|
onSelect={setSelectedId}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 右侧:聊天窗口 */}
|
|
|
|
|
|
<ChatWindow
|
|
|
|
|
|
contact={selectedContact}
|
|
|
|
|
|
messages={messages.filter(m => m.contactId === selectedId)}
|
|
|
|
|
|
onSend={(text) => setMessages(prev => [...prev, { text, contactId: selectedId!, timestamp: Date.now() }])}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
2026-04-29 22:19:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-04-29 23:36:32 +08:00
|
|
|
|
> [!diagram] 状态提升流程
|
|
|
|
|
|
> ```mermaid
|
|
|
|
|
|
> graph LR
|
|
|
|
|
|
> A[ContactList] -- "onSelect(id)" --> P[ChatApp - State]
|
|
|
|
|
|
> P -- "contact" --> B[ChatWindow]
|
|
|
|
|
|
> P -- "onSend(text)" --> P
|
|
|
|
|
|
> B -- "sendMessage(text)" --> P
|
|
|
|
|
|
> style P fill:#4FC08D,color:#fff
|
|
|
|
|
|
> ```
|
|
|
|
|
|
|
|
|
|
|
|
> [!tip] 何时应该使用状态提升?
|
|
|
|
|
|
> 当兄弟组件数量少(≤ 3 个)、关系固定、状态不频繁变化时,状态提升比引入状态管理库更简单、更可维护。
|
2026-04-29 22:19:51 +08:00
|
|
|
|
|
|
|
|
|
|
## 状态管理库方案
|
|
|
|
|
|
|
2026-04-29 23:36:32 +08:00
|
|
|
|
当状态需要在整个应用中流转、涉及复杂操作逻辑或多条副作用时,Zustand / Redux Toolkit 等库提供了集中式状态容器。
|
|
|
|
|
|
|
2026-04-29 22:19:51 +08:00
|
|
|
|
```tsx
|
2026-04-29 23:36:32 +08:00
|
|
|
|
// Zustand:极简 API,按 slice 订阅避免全量重渲染
|
2026-04-29 22:19:51 +08:00
|
|
|
|
import { create } from "zustand";
|
|
|
|
|
|
|
2026-04-29 23:36:32 +08:00
|
|
|
|
interface CartState {
|
|
|
|
|
|
items: CartItem[];
|
|
|
|
|
|
addItem: (item: CartItem) => void;
|
|
|
|
|
|
removeItem: (id: string) => void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const useCartStore = create<CartState>((set) => ({
|
2026-04-29 22:19:51 +08:00
|
|
|
|
items: [],
|
|
|
|
|
|
addItem: (item) => set(state => ({ items: [...state.items, item] })),
|
2026-04-29 23:36:32 +08:00
|
|
|
|
removeItem: (id) => set(state => ({ items: state.items.filter(i => i.id !== id) })),
|
2026-04-29 22:19:51 +08:00
|
|
|
|
}));
|
|
|
|
|
|
|
2026-04-29 23:36:32 +08:00
|
|
|
|
function ProductCard({ product }: { product: Product }) {
|
|
|
|
|
|
const addItem = useCartStore(s => s.addItem); // 精确订阅,仅在 addItem 引用变化时重渲染
|
2026-04-29 22:19:51 +08:00
|
|
|
|
return <button onClick={() => addItem(product)}>加入购物车</button>;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function CartIcon() {
|
2026-04-29 23:36:32 +08:00
|
|
|
|
const items = useCartStore(s => s.items); // 仅 items 变化时重渲染
|
2026-04-29 22:19:51 +08:00
|
|
|
|
return <Badge>{items.length}</Badge>;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 两者毫无关联,但共享同一份状态
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-04-29 23:36:32 +08:00
|
|
|
|
> [!note] Zustand vs Redux Toolkit 选型建议
|
|
|
|
|
|
> - **Zustand**:API 简洁、样板代码极少、支持 immer 中间件,适合中小型项目和个人项目
|
|
|
|
|
|
> - **Redux Toolkit**:生态成熟、DevTools 体验好、middleware 体系完善(thunk/saga),适合大型团队协作项目
|
|
|
|
|
|
> - 详见 [[hhs/REACT/3. 生态工具篇/09-状态管理.md]]
|
|
|
|
|
|
|
2026-04-29 22:19:51 +08:00
|
|
|
|
## Ref Imperative API(命令式通信)
|
|
|
|
|
|
|
2026-04-29 23:36:32 +08:00
|
|
|
|
大多数情况下 React 推崇声明式通信(Props + Callback),但在需要**直接操控子组件实例行为**时使用 refs。典型场景:触发自定义验证、聚焦输入框、播放动画。
|
|
|
|
|
|
|
2026-04-29 22:19:51 +08:00
|
|
|
|
```tsx
|
|
|
|
|
|
const formRef = useRef<FormHandle>(null);
|
|
|
|
|
|
|
|
|
|
|
|
function Parent() {
|
|
|
|
|
|
const handleSubmit = () => {
|
2026-04-29 23:36:32 +08:00
|
|
|
|
formRef.current?.validate(); // 命令子组件执行
|
2026-04-29 22:19:51 +08:00
|
|
|
|
formRef.current?.reset();
|
|
|
|
|
|
};
|
2026-04-29 23:36:32 +08:00
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<ChildForm ref={formRef} />
|
|
|
|
|
|
<button onClick={handleSubmit}>提交</button>
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
2026-04-29 22:19:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const ChildForm = forwardRef<FormHandle>((props, ref) => {
|
|
|
|
|
|
useImperativeHandle(ref, () => ({
|
|
|
|
|
|
validate: () => validator.validate(),
|
|
|
|
|
|
reset: () => setFields(initialValues),
|
|
|
|
|
|
}));
|
2026-04-29 23:36:32 +08:00
|
|
|
|
|
2026-04-29 22:19:51 +08:00
|
|
|
|
return <form>...</form>;
|
|
|
|
|
|
});
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-04-29 23:36:32 +08:00
|
|
|
|
> [!warning] 谨慎使用 Imperative Refs
|
|
|
|
|
|
> Imperative API 打破了 React 的声明式范式。能用 declarative(状态驱动 UI)解决的问题,永远不要上 imperative(直接操控 DOM)。过度使用会导致:难以测试、时序 bug、调试困难。
|
|
|
|
|
|
|
2026-04-29 22:19:51 +08:00
|
|
|
|
## 决策树
|
|
|
|
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
|
|
graph TD
|
2026-04-29 23:36:32 +08:00
|
|
|
|
START["需要组件通信?"] -->|"否"| DONE["无需处理 ✅"]
|
|
|
|
|
|
START -->|"是"| RANGE["通信范围?"]
|
|
|
|
|
|
|
|
|
|
|
|
RANGE --> PARENT_CHILD["仅父子两层<br/>(1-2 级深度)"]
|
|
|
|
|
|
RANGE --> DEEP["多层级穿透<br/>(≥ 3 级深度)"]
|
|
|
|
|
|
RANGE --> SIBLING["兄弟组件 / 无关组件"]
|
|
|
|
|
|
RANGE --> IMPERATIVE["需命令式调用子组件方法"]
|
|
|
|
|
|
|
|
|
|
|
|
PARENT_CHILD --> PROP["Props + Callback ✅<br/>简单、直观、可追踪"]
|
|
|
|
|
|
DEEP --> CTX["Context ✅<br/>配合 useReducer 保证性能"]
|
|
|
|
|
|
SIBLING --> SLIFTING["状态提升到共同父 ✅"]
|
|
|
|
|
|
SIBLING --> STORE["状态管理库(复杂场景)"]
|
|
|
|
|
|
IMPERATIVE --> REF["forwardRef +<br/>useImperativeHandle ✅"]
|
|
|
|
|
|
|
|
|
|
|
|
style PROP fill:#4FC08D,color:#fff
|
|
|
|
|
|
style CTX fill:#F5A87D,color:#000
|
|
|
|
|
|
style SLIFTING fill:#61DAFB,color:#000
|
|
|
|
|
|
style STORE fill:#C084FC,color:#fff
|
|
|
|
|
|
style REF fill:#A0AEC0,color:#000
|
2026-04-29 22:19:51 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 关联笔记
|
2026-04-29 23:36:32 +08:00
|
|
|
|
|
|
|
|
|
|
- [[hhs/REACT/1. 基础篇/03-组件与 Props.md]] — Props 类型校验、组件拆分原则
|
|
|
|
|
|
- [[hhs/REACT/2. Hooks 篇/05-核心 Hooks.md]] — useContext / useRef 原理
|
|
|
|
|
|
- [[hhs/REACT/2. Hooks 篇/06-性能优化 Hooks.md]] — 配合 Context 的性能优化手段
|
|
|
|
|
|
- [[hhs/REACT/4. 进阶篇/12-HOC 与 Render Props.md]] — 高阶组件与 Render Props 进阶
|
|
|
|
|
|
- [[hhs/REACT/3. 生态工具篇/09-状态管理.md]] — Zustand / Redux Toolkit 选型对比
|
|
|
|
|
|
- [[hhs/REACT/5. 工程实践篇/15-性能优化.md]] — React.memo、虚拟列表、Bundle 分析
|