#React 中组件之间如何通信?
#一、是什么
组件通信是 React 开发中的核心话题。由于 React 采用单向数据流的设计理念,数据默认只能从父组件通过 props 向下传递给子组件。但实际应用中,组件之间的关系远不只是父子关系,还包括兄弟组件、跨层级组件、甚至完全无关系的组件之间的数据交互。
React 提供了多种通信方式来应对不同的场景,选择合适的通信方式对应用的可维护性至关重要。
#二、父组件向子组件通信(Props)
这是最基础也是最常用的通信方式,父组件通过 props 将数据和回调传递给子组件。
interface UserInfoProps {
name: string;
email: string;
role: 'admin' | 'user';
}
function UserInfo({ name, email, role }: UserInfoProps) {
return (
<div className="user-info">
<h2>{name}</h2>
<p>{email}</p>
<span className={`badge badge-${role}`}>{role}</span>
</div>
);
}
function App() {
return (
<UserInfo
name="张三"
email="zhangsan@example.com"
role="admin"
/>
);
}还可以通过 children prop 传递 JSX 子元素:
function Card({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div className="card">
<h3 className="card-title">{title}</h3>
<div className="card-body">{children}</div>
</div>
);
}
function App() {
return (
<Card title="用户详情">
<UserInfo name="张三" email="zhangsan@example.com" role="admin" />
</Card>
);
}#三、子组件向父组件通信(回调函数)
子组件通过调用父组件传入的回调函数来向上传递数据。
interface SearchBarProps {
onSearch: (query: string) => void;
}
function SearchBar({ onSearch }: SearchBarProps) {
const [query, setQuery] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSearch(query);
};
return (
<form onSubmit={handleSubmit}>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="搜索..."
/>
<button type="submit">搜索</button>
</form>
);
}
function SearchPage() {
const [results, setResults] = useState<Item[]>([]);
const handleSearch = async (query: string) => {
const data = await fetchSearchResults(query);
setResults(data);
};
return (
<div>
<SearchBar onSearch={handleSearch} />
<ResultList items={results} />
</div>
);
}#四、兄弟组件通信(状态提升)
当两个兄弟组件需要共享状态时,将状态提升到它们最近的共同父组件中管理。
function TemperatureInput({
scale,
temperature,
onTemperatureChange,
}: {
scale: 'c' | 'f';
temperature: string;
onTemperatureChange: (temp: string) => void;
}) {
const scaleNames = { c: '摄氏', f: '华氏' };
return (
<fieldset>
<legend>输入{scaleNames[scale]}温度:</legend>
<input
value={temperature}
onChange={e => onTemperatureChange(e.target.value)}
/>
</fieldset>
);
}
function toCelsius(fahrenheit: number) { return (fahrenheit - 32) * 5 / 9; }
function toFahrenheit(celsius: number) { return celsius * 9 / 5 + 32; }
function TemperatureCalculator() {
const [temperature, setTemperature] = useState('');
const [scale, setScale] = useState<'c' | 'f'>('c');
const celsius = scale === 'f' ? toCelsius(parseFloat(temperature)) : parseFloat(temperature);
const fahrenheit = scale === 'c' ? toFahrenheit(parseFloat(temperature)) : parseFloat(temperature);
return (
<div>
<TemperatureInput
scale="c"
temperature={scale === 'c' ? temperature : celsius.toFixed(2)}
onTemperatureChange={t => { setTemperature(t); setScale('c'); }}
/>
<TemperatureInput
scale="f"
temperature={scale === 'f' ? temperature : fahrenheit.toFixed(2)}
onTemperatureChange={t => { setTemperature(t); setScale('f'); }}
/>
</div>
);
}#五、跨层级通信(Context API)
当数据需要跨越多层组件传递时,使用 Context API 避免逐层传递 props(prop drilling)。
import { createContext, useContext, useState } from 'react';
interface Theme {
mode: 'light' | 'dark';
primaryColor: string;
}
interface ThemeContextValue {
theme: Theme;
toggleMode: () => void;
}
const ThemeContext = createContext<ThemeContextValue | null>(null);
function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error('useTheme 必须在 ThemeProvider 内部使用');
return ctx;
}
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>({
mode: 'light',
primaryColor: '#1890ff',
});
const toggleMode = () => {
setTheme(prev => ({
...prev,
mode: prev.mode === 'light' ? 'dark' : 'light',
}));
};
return (
<ThemeContext.Provider value={{ theme, toggleMode }}>
{children}
</ThemeContext.Provider>
);
}
function Header() {
const { theme, toggleMode } = useTheme();
return (
<header style={{ background: theme.mode === 'dark' ? '#333' : '#fff' }}>
<h1>我的应用</h1>
<button onClick={toggleMode}>
切换到{theme.mode === 'light' ? '暗色' : '亮色'}模式
</button>
</header>
);
}
function DeepNestedComponent() {
const { theme } = useTheme();
return <p style={{ color: theme.primaryColor }}>深层嵌套组件也能获取主题</p>;
}
function App() {
return (
<ThemeProvider>
<Header />
<main>
<section>
<DeepNestedComponent />
</section>
</main>
</ThemeProvider>
);
}#Context 的性能注意事项
Context 值变化时,所有消费该 Context 的组件都会重新渲染。可以通过拆分 Context 或使用 useMemo 优化:
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [mode, setMode] = useState<'light' | 'dark'>('light');
const value = useMemo(
() => ({
theme: { mode, primaryColor: '#1890ff' },
toggleMode: () => setMode(m => (m === 'light' ? 'dark' : 'light')),
}),
[mode]
);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}#六、全局状态管理(Redux / Zustand)
对于大型应用中的全局共享状态,通常使用专门的状态管理库。
#Zustand 示例(轻量级)
import { create } from 'zustand';
interface CartStore {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
total: () => number;
}
const useCartStore = create<CartStore>((set, get) => ({
items: [],
addItem: (item) => set(state => ({ items: [...state.items, item] })),
removeItem: (id) => set(state => ({
items: state.items.filter(i => i.id !== id),
})),
total: () => get().items.reduce((sum, item) => sum + item.price, 0),
}));
function CartButton() {
const itemCount = useCartStore(state => state.items.length);
return <button>购物车 ({itemCount})</button>;
}
function ProductCard({ product }: { product: Product }) {
const addItem = useCartStore(state => state.addItem);
return (
<div>
<h3>{product.name}</h3>
<p>¥{product.price}</p>
<button onClick={() => addItem({ id: product.id, name: product.name, price: product.price })}>
加入购物车
</button>
</div>
);
}#Redux Toolkit 示例
import { configureStore, createSlice, PayloadAction } from '@reduxjs/toolkit';
import { useSelector, useDispatch, Provider } from 'react-redux';
interface CounterState {
value: number;
}
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 } as CounterState,
reducers: {
increment: state => { state.value += 1; },
decrement: state => { state.value -= 1; },
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
},
});
const store = configureStore({
reducer: { counter: counterSlice.reducer },
});
type RootState = ReturnType<typeof store.getState>;
function Counter() {
const count = useSelector((state: RootState) => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<span>{count}</span>
<button onClick={() => dispatch(counterSlice.actions.increment())}>+1</button>
<button onClick={() => dispatch(counterSlice.actions.decrement())}>-1</button>
</div>
);
}#七、Ref 转发通信
通过 ref 和 useImperativeHandle,父组件可以直接调用子组件暴露的方法。
import { useRef, useImperativeHandle, forwardRef } from 'react';
interface ModalHandle {
open: () => void;
close: () => void;
}
const Modal = forwardRef<ModalHandle, { title: string; children: React.ReactNode }>(
function Modal({ title, children }, ref) {
const [visible, setVisible] = useState(false);
useImperativeHandle(ref, () => ({
open: () => setVisible(true),
close: () => setVisible(false),
}));
if (!visible) return null;
return (
<div className="modal-overlay">
<div className="modal">
<h2>{title}</h2>
{children}
<button onClick={() => setVisible(false)}>关闭</button>
</div>
</div>
);
}
);
function App() {
const modalRef = useRef<ModalHandle>(null);
return (
<div>
<button onClick={() => modalRef.current?.open()}>打开弹窗</button>
<Modal ref={modalRef} title="提示">
<p>这是弹窗内容</p>
</Modal>
</div>
);
}#八、事件总线模式
事件总线(Event Bus)允许任意组件之间通信,不受组件层级限制。但这种方式会使数据流变得难以追踪,应谨慎使用。
type EventHandler = (...args: any[]) => void;
class EventBus {
private events = new Map<string, Set<EventHandler>>();
on(event: string, handler: EventHandler) {
if (!this.events.has(event)) {
this.events.set(event, new Set());
}
this.events.get(event)!.add(handler);
return () => this.events.get(event)?.delete(handler);
}
emit(event: string, ...args: any[]) {
this.events.get(event)?.forEach(handler => handler(...args));
}
}
const bus = new EventBus();
function useEventBus(event: string, handler: EventHandler) {
useEffect(() => {
const unsubscribe = bus.on(event, handler);
return unsubscribe;
}, [event, handler]);
}
function NotificationSender() {
return (
<button onClick={() => bus.emit('notify', { message: '新消息到达', type: 'info' })}>
发送通知
</button>
);
}
function NotificationListener() {
const [notifications, setNotifications] = useState<string[]>([]);
useEventBus('notify', useCallback((data: { message: string }) => {
setNotifications(prev => [...prev, data.message]);
}, []));
return (
<ul>
{notifications.map((msg, i) => <li key={i}>{msg}</li>)}
</ul>
);
}#九、通信方式选择指南
| 通信场景 | 推荐方式 | 适用范围 |
|---|---|---|
| 父 → 子 | props | 直接父子 |
| 子 → 父 | 回调函数 | 直接父子 |
| 兄弟组件 | 状态提升 | 有共同父组件 |
| 跨层级 | Context API | 主题、语言、认证等低频更新数据 |
| 全局状态 | Zustand / Redux | 大型应用复杂状态 |
| 命令式操作 | ref + useImperativeHandle | 弹窗、播放器等命令式交互 |
| 松耦合通信 | 事件总线 | 微前端、插件系统等 |
#十、总结
React 组件通信方式的选择应遵循以下原则:
- 能用 props 解决就用 props,保持单向数据流
- 就近原则:状态应放在需要它的最近公共祖先
- Context 用于低频更新的全局数据(主题、国际化、用户信息)
- 高频更新的全局状态使用专门的状态管理库
- 避免滥用事件总线,它会让数据流变得不可预测
- ref 通信用于命令式操作,不要用来替代声明式数据流