在 React 应用中,组件间的数据传递是构建复杂 UI 的核心课题。根据组件关系的不同,通信方式可分为两大场景:父子组件通信 和 跨级/非父子组件通信。本文将系统梳理这两类场景下的主流实现方案及其适用场景。
一、父子组件通信
1. 父组件向子组件传递数据 ------ Props
这是 React 中最基础、最常用的单向数据流方式。父组件通过 JSX 属性将数据传递给子组件,子组件通过 props对象接收。
javascript
// 父组件
function Parent() {
const user = { name: 'Alice', age: 30 };
return <Child info={user} />;
}
// 子组件
function Child(props) {
return <p>{props.info.name}</p>;
}
特点:数据流清晰、可预测,符合 React 的设计哲学。
2. 子组件向父组件传递数据 ------ 回调函数
父组件将一个函数作为 props 传递给子组件,子组件在合适的时机(如点击、输入变化)调用该函数并携带参数,从而实现子传父。
javascript
function Parent() {
const handleChildMsg = (msg) => {
console.log('来自子组件的消息:', msg);
};
return <Child onSend={handleChildMsg} />;
}
function Child({ onSend }) {
return <button onClick={() => onSend('Hello!')}>发送</button>;
}
本质:依然是 Props 传递,只是传递的是函数引用。常用于表单提交、事件通知等需要子组件反馈的场景。
二、跨过父子组件的直接通信
当组件之间没有直接的父子关系(如兄弟组件、隔代组件、完全不相关的组件),就需要借助额外的机制实现"直接"通信。
1. Context API(内置方案)
适用于祖先与后代之间共享数据,无需逐层传递 props。
- 创建 Context :
const MyCtx = React.createContext(defaultValue); - 提供者 :
<MyCtx.Provider value={...}>包裹子树 - 消费者 :使用
useContext(MyCtx)Hook 或<MyCtx.Consumer>组件
javascript
const ThemeContext = React.createContext('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
return <ThemedButton />;
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return <button className={theme}>按钮</button>;
}
适用场景:主题、语言、认证信息等全局性数据,且组件层级较深时。
2. 全局状态管理库(Redux / Zustand / MobX)
适用于大型应用,多个组件需要共享和修改同一份状态。
- Redux :单一 store,通过 action → reducer 更新状态,组件通过
connect或useSelector读取。 - Zustand:轻量级,基于 hooks,直接创建 store 并在任何组件中使用。
javascript
import { create } from 'zustand';
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
function Counter() {
const count = useStore((s) => s.count);
const increment = useStore((s) => s.increment);
return <button onClick={increment}>{count}</button>;
}
适用场景:复杂业务状态,多组件频繁读写,需要时间旅行调试等高级功能。
3. Event Bus(发布/订阅模式)
适合简单场景下任意两个组件之间的通信,不依赖组件树结构。可使用第三方库如 mitt、eventemitter3或自己实现。
javascript
// eventBus.js
import mitt from 'mitt';
export const bus = mitt();
// ComponentA
bus.emit('customEvent', { data: 'hello' });
// ComponentB
useEffect(() => {
const handler = (payload) => console.log(payload);
bus.on('customEvent', handler);
return () => bus.off('customEvent', handler);
}, []);
注意:Event Bus 不受 React 生命周期管控,需手动清理订阅,否则易导致内存泄漏。应谨慎使用。
4. Refs + ImperativeHandle(特定场景)
父组件可以通过 useRef获取子组件的实例或暴露的方法,从而直接调用子组件内部逻辑(例如聚焦输入框)。这不算严格的数据通信,但可实现指令式交互。
javascript
function Child(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus(),
}));
return <input ref={inputRef} />;
}
Child = forwardRef(Child);
function Parent() {
const childRef = useRef();
return (
<>
<Child ref={childRef} />
<button onClick={() => childRef.current.focus()}>聚焦</button>
</>
);
}
三、选择建议
| 通信类型 | 推荐方案 | 适用场景 |
|---|---|---|
| 父→子 | Props | 所有基本父子传递 |
| 子→父 | 回调函数 | 子组件触发父组件行为 |
| 祖→孙 / 深层嵌套 | Context API | 主题、语言、认证信息等全局性数据 |
| 兄弟 / 任意跨级 | 状态管理库(Redux/Zustand) | 复杂业务状态,多组件频繁读写 |
| 临时性跨组件 | Event Bus | 简单通知、解耦的一次性事件 |
最佳实践 :优先使用 Props 提升 (将状态提升到共同父组件)或 Context,只有当项目规模较大、状态逻辑复杂时才引入外部状态管理库。Event Bus 应谨慎使用,因为它跳过了 React 的数据流体系,不利于调试和追踪。
掌握这些通信方式,你就能在 React 项目中灵活应对各种组件间数据交换的需求,写出结构清晰、易于维护的代码。