Files
cs-note/hhs/REACT/4. 进阶篇/11-组件通信模式.md
T
2026-05-24 11:42:38 +08:00

341 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [React, Component Communication, Props, Context, State Lifting, Frontend]
create time: 2026-04-29 22:10
---
# 组件通信模式
## 概述
React 的核心理念是 **"单向数据流"** ——数据从父组件流向子组件,事件从子组件冒泡回父组件。但在真实项目中,组件层级可以很深、结构可以很散,跨组件共享状态成为日常需求。
本文档梳理 React 中所有主流的组件通信方式,从最基础的 Props 到状态管理库,帮助你根据实际场景选择最合适方案。
> [!question] 思考题
> 假设你有一个 6 层深的组件树,最顶层需要把 `theme` 传给第 6 层的按钮组件。你会怎么做?逐层透传 Props 似乎笨拙,但引入 Context 又可能引发不必要的重渲染——你怎么权衡?
## 通信方向全景图
```mermaid
graph TD
A["父 → 子"] --> B["Props(最基础)"]
A --> C["Context Provider(跨层级)"]
A --> D["状态管理库(全局共享)"]
E["子 → 父"] --> F["回调 Prop(事件驱动)"]
E --> G["Refs(imperative)"]
H["兄弟组件"] --> I["状态提升到共同父组件"]
H --> J["通过共同祖先通信"]
H --> K["状态管理库 / 发布订阅"]
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();
return <Header user={user} />;
// Header 不需要 user,但深层的 Avatar 需要!
}
function Header({ user }) {
return (
<nav>
<Logo />
{/* ❌ 无需 user,却被迫接收 */}
<Avatar user={user} />
</nav>
);
}
```
> [!tip] 何时该容忍 Props Drill?
> 传递 2-3 层 Props 是完全合理的,甚至值得鼓励——它让数据流向清晰可见。只有超过 4 层时才考虑替代方案。
### 解决方案对比
| 方法 | 适合场景 | 额外依赖 |
|------|----------|---------|
| **Context** | 少量深层传递(主题、用户信息、语言设置) | 无 |
| **自定义 Hook + Props 重组织** | 中等深度(3-4 层),只需中间层不暴露 | 无 |
| **状态管理库** | 频繁变化、多组件共享 | Zustand / Redux Toolkit |
| **Render Props / HOC** | 复用逻辑而非单纯传值 | 无 |
## Callback Prop(子 → 父)
这是 React 中最基础也最重要的反向通信方式:**父组件传递一个函数作为 Prop,子组件在适当时机调用它**。
```tsx
interface FormProps {
onSubmit: (data: FormValues) => Promise<void>;
}
function LoginForm({ onSubmit }: FormProps) {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const data = collectFormData();
onSubmit(data); // ✅ 通知父组件
};
return <form onSubmit={handleSubmit}><Button type="submit">登录</Button></form>;
}
// 父组件
function App() {
const handleLogin = async (data: FormValues) => {
await api.login(data);
navigate("/dashboard");
};
return <LoginForm onSubmit={handleLogin} />;
}
```
> [!note] 为什么不用 children 做通信?
> children 是 UI 内容插槽,不是通信机制。回调 Prop 保持关注点分离——子组件只关心"什么时候触发",父组件决定"触发后做什么"。
> [!important] 性能提示:使用 useCallback
> 如果子组件用 `React.memo` 包裹,每次父组件重新渲染都会创建新的函数引用,导致子组件无效重渲染。用 `useCallback` 缓存回调函数:
> ```tsx
> const handleLogin = useCallback(async (data: FormValues) => { ... }, []);
> ```
## Context 跨层级通信
当 Props 穿透超过 3-4 层时,Context 是最轻量的替代方案。**它的本质是一个"隐式 Prop"——Provider 上方的任意后代都可以直接消费,无需经过中间组件。**
```tsx
const UserContext = createContext<User | null>(null);
function UserProfileProvider({ children }: { children: React.ReactNode }) {
const user = useDatabaseUser(userId);
return (
<UserContext.Provider value={user}>
{children} {/* Avatar、Settings 任意深度都可消费 */}
</UserContext.Provider>
);
}
function OrderHistory() {
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>
);
}
```
> [!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 个)、关系固定、状态不频繁变化时,状态提升比引入状态管理库更简单、更可维护。
## 状态管理库方案
当状态需要在整个应用中流转、涉及复杂操作逻辑或多条副作用时,Zustand / Redux Toolkit 等库提供了集中式状态容器。
```tsx
// Zustand:极简 API,按 slice 订阅避免全量重渲染
import { create } from "zustand";
interface CartState {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
}
const useCartStore = create<CartState>((set) => ({
items: [],
addItem: (item) => set(state => ({ items: [...state.items, item] })),
removeItem: (id) => set(state => ({ items: state.items.filter(i => i.id !== id) })),
}));
function ProductCard({ product }: { product: Product }) {
const addItem = useCartStore(s => s.addItem); // 精确订阅,仅在 addItem 引用变化时重渲染
return <button onClick={() => addItem(product)}>加入购物车</button>;
}
function CartIcon() {
const items = useCartStore(s => s.items); // 仅 items 变化时重渲染
return <Badge>{items.length}</Badge>;
}
// 两者毫无关联,但共享同一份状态
```
> [!note] Zustand vs Redux Toolkit 选型建议
> - **Zustand**:API 简洁、样板代码极少、支持 immer 中间件,适合中小型项目和个人项目
> - **Redux Toolkit**:生态成熟、DevTools 体验好、middleware 体系完善(thunk/saga),适合大型团队协作项目
> - 详见 [[hhs/REACT/3. 生态工具篇/09-状态管理.md]]
## Ref Imperative API(命令式通信)
大多数情况下 React 推崇声明式通信(Props + Callback),但在需要**直接操控子组件实例行为**时使用 refs。典型场景:触发自定义验证、聚焦输入框、播放动画。
```tsx
const formRef = useRef<FormHandle>(null);
function Parent() {
const handleSubmit = () => {
formRef.current?.validate(); // 命令子组件执行
formRef.current?.reset();
};
return (
<>
<ChildForm ref={formRef} />
<button onClick={handleSubmit}>提交</button>
</>
);
}
const ChildForm = forwardRef<FormHandle>((props, ref) => {
useImperativeHandle(ref, () => ({
validate: () => validator.validate(),
reset: () => setFields(initialValues),
}));
return <form>...</form>;
});
```
> [!warning] 谨慎使用 Imperative Refs
> Imperative API 打破了 React 的声明式范式。能用 declarative(状态驱动 UI)解决的问题,永远不要上 imperative(直接操控 DOM)。过度使用会导致:难以测试、时序 bug、调试困难。
## 决策树
```mermaid
graph TD
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
```
## 关联笔记
- [[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 分析