跳到主要内容

React 组件通信方案

问题

React 组件之间如何通信?父子、兄弟、跨层级组件通信有哪些方案?

答案

React 组件通信主要有以下几种方式:

通信方式适用场景复杂度
Props父 → 子
回调函数子 → 父
状态提升兄弟组件⭐⭐
Context跨层级⭐⭐
Ref父 → 子(命令式)⭐⭐
状态管理库全局状态⭐⭐⭐
发布订阅任意组件⭐⭐

1. Props(父传子)

最基本的通信方式,通过 props 将数据从父组件传递给子组件:

interface UserCardProps {
name: string;
age: number;
avatar?: string;
}

// 子组件
function UserCard({ name, age, avatar }: UserCardProps) {
return (
<div className="user-card">
{avatar && <img src={avatar} alt={name} />}
<h3>{name}</h3>
<p>年龄: {age}</p>
</div>
);
}

// 父组件
function UserList() {
const users = [
{ id: 1, name: 'Alice', age: 25 },
{ id: 2, name: 'Bob', age: 30 },
];

return (
<div>
{users.map(user => (
<UserCard key={user.id} name={user.name} age={user.age} />
))}
</div>
);
}

Props 传递函数和组件

interface ModalProps {
title: string;
onClose: () => void;
footer?: React.ReactNode;
children: React.ReactNode;
}

function Modal({ title, onClose, footer, children }: ModalProps) {
return (
<div className="modal">
<header>
<h2>{title}</h2>
<button onClick={onClose}>×</button>
</header>
<main>{children}</main>
{footer && <footer>{footer}</footer>}
</div>
);
}

// 使用
function App() {
const [open, setOpen] = useState(false);

return (
<Modal
title="确认删除"
onClose={() => setOpen(false)}
footer={<Button>确定</Button>}
>
<p>确定要删除这条记录吗?</p>
</Modal>
);
}

2. 回调函数(子传父)

子组件通过调用父组件传递的回调函数,将数据传递给父组件:

interface SearchInputProps {
onSearch: (keyword: string) => void;
placeholder?: string;
}

// 子组件
function SearchInput({ onSearch, placeholder }: SearchInputProps) {
const [value, setValue] = useState('');

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSearch(value); // 调用父组件的回调
};

return (
<form onSubmit={handleSubmit}>
<input
value={value}
onChange={e => setValue(e.target.value)}
placeholder={placeholder}
/>
<button type="submit">搜索</button>
</form>
);
}

// 父组件
function SearchPage() {
const [results, setResults] = useState<string[]>([]);

const handleSearch = async (keyword: string) => {
// 接收子组件传来的数据
const data = await fetchSearchResults(keyword);
setResults(data);
};

return (
<div>
<SearchInput onSearch={handleSearch} placeholder="输入关键词" />
<ul>
{results.map(item => <li key={item}>{item}</li>)}
</ul>
</div>
);
}

多个数据的传递

interface FormData {
username: string;
email: string;
password: string;
}

interface RegisterFormProps {
onSubmit: (data: FormData) => Promise<void>;
onCancel: () => void;
}

function RegisterForm({ onSubmit, onCancel }: RegisterFormProps) {
const [formData, setFormData] = useState<FormData>({
username: '',
email: '',
password: '',
});
const [loading, setLoading] = useState(false);

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
await onSubmit(formData); // 传递整个表单数据
} finally {
setLoading(false);
}
};

return (
<form onSubmit={handleSubmit}>
<input
value={formData.username}
onChange={e => setFormData(prev => ({ ...prev, username: e.target.value }))}
placeholder="用户名"
/>
<input
type="email"
value={formData.email}
onChange={e => setFormData(prev => ({ ...prev, email: e.target.value }))}
placeholder="邮箱"
/>
<input
type="password"
value={formData.password}
onChange={e => setFormData(prev => ({ ...prev, password: e.target.value }))}
placeholder="密码"
/>
<button type="submit" disabled={loading}>
{loading ? '注册中...' : '注册'}
</button>
<button type="button" onClick={onCancel}>取消</button>
</form>
);
}

3. 状态提升(兄弟组件通信)

将共享状态提升到最近的公共父组件:

// 兄弟组件 A:选择器
interface SelectorProps {
value: string;
onChange: (value: string) => void;
options: string[];
}

function CategorySelector({ value, onChange, options }: SelectorProps) {
return (
<select value={value} onChange={e => onChange(e.target.value)}>
{options.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
);
}

// 兄弟组件 B:列表
interface ProductListProps {
category: string;
}

function ProductList({ category }: ProductListProps) {
const [products, setProducts] = useState<Product[]>([]);

useEffect(() => {
fetchProducts(category).then(setProducts);
}, [category]);

return (
<ul>
{products.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
);
}

// 父组件:管理共享状态
function ProductPage() {
// 状态提升到父组件
const [category, setCategory] = useState('all');
const categories = ['all', 'electronics', 'clothing', 'books'];

return (
<div>
<CategorySelector
value={category}
onChange={setCategory}
options={categories}
/>
<ProductList category={category} />
</div>
);
}
状态提升的问题

状态提升会导致父组件重渲染时,所有子组件都重渲染。对于大型应用,可以考虑:

  1. 使用 React.memo 优化子组件
  2. 使用 Context 避免 props 层层传递
  3. 使用状态管理库

4. Context(跨层级通信)

Context 用于在组件树中共享数据,避免 props 逐层传递(Prop Drilling):

// 1. 创建 Context
interface ThemeContextType {
theme: 'light' | 'dark';
toggleTheme: () => void;
}

const ThemeContext = createContext<ThemeContextType | null>(null);

// 自定义 Hook,方便使用
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}

// 2. 创建 Provider
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');

const toggleTheme = useCallback(() => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
}, []);

const value = useMemo(() => ({ theme, toggleTheme }), [theme, toggleTheme]);

return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}

// 3. 在任意层级消费
function DeepNestedButton() {
const { theme, toggleTheme } = useTheme();

return (
<button
onClick={toggleTheme}
style={{
background: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff',
}}
>
切换主题
</button>
);
}

// 4. 在顶层包裹 Provider
function App() {
return (
<ThemeProvider>
<Header />
<Main />
<Footer />
</ThemeProvider>
);
}

多个 Context 组合

// 用户 Context
interface User {
id: string;
name: string;
role: 'admin' | 'user';
}

const UserContext = createContext<User | null>(null);
const UserDispatchContext = createContext<React.Dispatch<UserAction> | null>(null);

// 组合多个 Provider
function AppProviders({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider>
<UserProvider>
<NotificationProvider>
{children}
</NotificationProvider>
</UserProvider>
</ThemeProvider>
);
}
Context 性能优化

Context 值变化时,所有消费者都会重渲染。优化方式:

  1. 拆分 Context:将频繁变化和稳定的值分开
  2. useMemo 包裹 value:避免每次渲染创建新对象
  3. 组件拆分:只让需要数据的组件订阅 Context

5. Ref(命令式通信)

使用 useImperativeHandle 暴露子组件的方法给父组件:

// 子组件暴露的方法类型
interface InputRef {
focus: () => void;
clear: () => void;
getValue: () => string;
}

interface CustomInputProps {
placeholder?: string;
}

// 子组件
const CustomInput = forwardRef<InputRef, CustomInputProps>(
function CustomInput({ placeholder }, ref) {
const inputRef = useRef<HTMLInputElement>(null);
const [value, setValue] = useState('');

// 暴露方法给父组件
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
clear: () => setValue(''),
getValue: () => value,
}), [value]);

return (
<input
ref={inputRef}
value={value}
onChange={e => setValue(e.target.value)}
placeholder={placeholder}
/>
);
}
);

// 父组件
function Form() {
const inputRef = useRef<InputRef>(null);

const handleSubmit = () => {
const value = inputRef.current?.getValue();
console.log('提交:', value);
inputRef.current?.clear();
};

return (
<div>
<CustomInput ref={inputRef} placeholder="请输入" />
<button onClick={() => inputRef.current?.focus()}>聚焦</button>
<button onClick={handleSubmit}>提交</button>
</div>
);
}
React 19 变化

React 19 中 ref 可以直接作为 prop 传递,无需 forwardRef

// React 19
function CustomInput({ ref, placeholder }: { ref?: React.Ref<InputRef>; placeholder?: string }) {
useImperativeHandle(ref, () => ({
focus: () => { /* ... */ },
}));
return <input placeholder={placeholder} />;
}

6. 发布订阅模式

适用于完全解耦的组件通信:

// 事件总线
type EventCallback<T = any> = (data: T) => void;

class EventBus {
private events: Map<string, Set<EventCallback>> = new Map();

on<T>(event: string, callback: EventCallback<T>) {
if (!this.events.has(event)) {
this.events.set(event, new Set());
}
this.events.get(event)!.add(callback);

// 返回取消订阅函数
return () => this.off(event, callback);
}

off<T>(event: string, callback: EventCallback<T>) {
this.events.get(event)?.delete(callback);
}

emit<T>(event: string, data: T) {
this.events.get(event)?.forEach(callback => callback(data));
}
}

const eventBus = new EventBus();

// 自定义 Hook
function useEventBus<T>(event: string, handler: EventCallback<T>) {
useEffect(() => {
return eventBus.on(event, handler);
}, [event, handler]);
}

// 组件 A:发送事件
function CartButton({ productId }: { productId: string }) {
const handleAddToCart = () => {
eventBus.emit('cart:add', { productId, quantity: 1 });
};

return <button onClick={handleAddToCart}>加入购物车</button>;
}

// 组件 B:监听事件
function CartBadge() {
const [count, setCount] = useState(0);

useEventBus('cart:add', useCallback(() => {
setCount(c => c + 1);
}, []));

return <span className="badge">{count}</span>;
}
注意事项

发布订阅模式绕过了 React 的数据流,可能导致:

  1. 难以追踪数据来源
  2. 容易产生内存泄漏(忘记取消订阅)
  3. 不利于调试

建议仅在特定场景使用,如全局通知、跨页面通信等。

7. 状态管理库

对于复杂的全局状态,推荐使用状态管理库:

Zustand(推荐)

import { create } from 'zustand';

interface CartStore {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
clearCart: () => void;
totalPrice: () => number;
}

const useCartStore = create<CartStore>((set, get) => ({
items: [],

addItem: (item) => set((state) => ({
items: [...state.items, item]
})),

removeItem: (id) => set((state) => ({
items: state.items.filter(item => item.id !== id)
})),

clearCart: () => set({ items: [] }),

totalPrice: () => get().items.reduce((sum, item) => sum + item.price, 0),
}));

// 任意组件使用
function CartIcon() {
const itemCount = useCartStore(state => state.items.length);
return <span>{itemCount}</span>;
}

function CartList() {
const { items, removeItem } = useCartStore();
return (
<ul>
{items.map(item => (
<li key={item.id}>
{item.name}
<button onClick={() => removeItem(item.id)}>删除</button>
</li>
))}
</ul>
);
}

Redux Toolkit

import { createSlice, configureStore } from '@reduxjs/toolkit';
import { useSelector, useDispatch } from 'react-redux';

// Slice
const cartSlice = createSlice({
name: 'cart',
initialState: { items: [] as CartItem[] },
reducers: {
addItem: (state, action) => {
state.items.push(action.payload);
},
removeItem: (state, action) => {
state.items = state.items.filter(item => item.id !== action.payload);
},
},
});

// Store
const store = configureStore({
reducer: { cart: cartSlice.reducer },
});

type RootState = ReturnType<typeof store.getState>;

// 组件使用
function CartList() {
const items = useSelector((state: RootState) => state.cart.items);
const dispatch = useDispatch();

return (
<ul>
{items.map(item => (
<li key={item.id}>
{item.name}
<button onClick={() => dispatch(cartSlice.actions.removeItem(item.id))}>
删除
</button>
</li>
))}
</ul>
);
}

通信方式选择指南

场景推荐方案
简单父子通信Props + 回调函数
2-3 层传递Props 或 Context
表单联动状态提升
主题/国际化Context
用户登录状态Context 或状态管理库
购物车/复杂业务Zustand / Redux
跨页面通知发布订阅

常见面试问题

Q1: React 组件通信有哪些方式?

答案

方式方向场景
Props父 → 子传递数据和配置
回调函数子 → 父子组件触发父组件更新
状态提升兄弟共享状态到公共父组件
Context跨层级避免 Prop Drilling
Ref父 → 子命令式操作子组件
状态管理库全局复杂状态管理
发布订阅任意完全解耦的通信

Q2: Context 会导致性能问题吗?如何优化?

答案

Context 值变化时,所有消费者都会重渲染,可能导致性能问题。

优化方案

  1. 拆分 Context
// ❌ 耦合在一起
const AppContext = createContext({ user, theme, settings });

// ✅ 分开
const UserContext = createContext(user);
const ThemeContext = createContext(theme);
  1. useMemo 包裹 value
const value = useMemo(() => ({ user, updateUser }), [user]);
<UserContext.Provider value={value}>{children}</UserContext.Provider>
  1. 组件拆分
// 只有 UserName 订阅 Context,父组件不受影响
function UserName() {
const user = useContext(UserContext);
return <span>{user.name}</span>;
}

Q3: 什么时候用 Context,什么时候用 Redux?

答案

场景ContextRedux/Zustand
主题切换
用户登录状态
国际化
购物车
复杂表单状态
需要时间旅行调试
需要中间件

简单规则

  • 低频更新 + 简单数据 → Context
  • 高频更新 + 复杂逻辑 → 状态管理库

Q4: 如何避免 Prop Drilling?

答案

Prop Drilling 是指 props 需要经过多层组件传递才能到达目标组件。

// ❌ Prop Drilling
<App user={user}>
<Layout user={user}>
<Sidebar user={user}>
<UserMenu user={user} /> // 终于用到了
</Sidebar>
</Layout>
</App>

解决方案

  1. Context
const UserContext = createContext(user);
// UserMenu 直接获取
const user = useContext(UserContext);
  1. 组件组合(Composition)
function App() {
return (
<Layout sidebar={<UserMenu user={user} />}>
<Content />
</Layout>
);
}
  1. 状态管理库
// 任意组件直接获取
const user = useUserStore(state => state.user);

Q5: Context 和状态管理库(如 Zustand、Redux)各适合什么场景?

答案

两者都能实现全局状态共享,但设计理念和适用场景有明显差异:

对比维度Context状态管理库(Zustand/Redux)
更新粒度所有消费者全部重渲染可通过 selector 精确订阅
适合的数据低频变化(主题、语言、用户信息)高频变化(表单、列表、实时数据)
中间件支持(日志、持久化、异步)
DevToolsRedux DevTools / Zustand DevTools
代码量稍多(但 Zustand 很轻量)
学习成本中等

Context 的 re-render 问题

// ❌ Context 值变化时,所有消费者都会重渲染
interface AppState {
user: { name: string };
theme: 'light' | 'dark';
counter: number; // 频繁变化
}

const AppContext = createContext<AppState>(null!);

function Header() {
const { user } = useContext(AppContext);
console.log('Header 渲染'); // counter 变化时也会触发!
return <h1>{user.name}</h1>;
}

function Counter() {
const { counter } = useContext(AppContext);
return <span>{counter}</span>;
}

// ✅ 使用 Zustand,只有订阅的 slice 变化才重渲染
import { create } from 'zustand';

interface AppStore {
user: { name: string };
theme: 'light' | 'dark';
counter: number;
increment: () => void;
}

const useAppStore = create<AppStore>((set) => ({
user: { name: 'Alice' },
theme: 'light',
counter: 0,
increment: () => set((state) => ({ counter: state.counter + 1 })),
}));

function Header() {
// 只订阅 user,counter 变化不会触发重渲染
const user = useAppStore((state) => state.user);
console.log('Header 渲染'); // 只有 user 变化才触发
return <h1>{user.name}</h1>;
}

function Counter() {
const counter = useAppStore((state) => state.counter);
const increment = useAppStore((state) => state.increment);
return <button onClick={increment}>{counter}</button>;
}

选择建议

// Context 适合的场景:低频变化、全局配置型数据
// - 主题切换(light/dark)
// - 国际化语言
// - 当前登录用户信息
// - 功能开关(Feature Flags)

const ThemeContext = createContext<'light' | 'dark'>('light');
const LocaleContext = createContext<string>('zh-CN');

// 状态管理库适合的场景:高频变化、复杂业务逻辑
// - 购物车
// - 表单联动
// - 实时聊天消息
// - 需要持久化 / 时间旅行调试的状态

const useChatStore = create<ChatStore>((set) => ({
messages: [],
sendMessage: (msg: string) =>
set((state) => ({
messages: [...state.messages, { text: msg, time: Date.now() }],
})),
}));
实际项目建议

大多数项目可以同时使用两者:用 Context 管理主题和用户信息等"配置型"数据,用 Zustand 管理"业务型"状态。不要把所有状态都塞进一个方案里。

Q6: 如何实现跨层级组件通信而不用 Context?

答案

除了 Context,还有多种方式可以实现跨层级通信,各有优劣:

方案 1:发布订阅 / EventBus

// 自定义 Hook 封装 EventBus
type Handler<T = any> = (data: T) => void;

class EventEmitter {
private listeners = new Map<string, Set<Handler>>();

on<T>(event: string, handler: Handler<T>): () => void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(handler);
return () => this.listeners.get(event)?.delete(handler);
}

emit<T>(event: string, data: T): void {
this.listeners.get(event)?.forEach((fn) => fn(data));
}
}

const emitter = new EventEmitter();

function useEvent<T>(event: string, handler: Handler<T>): void {
const savedHandler = useRef(handler);
savedHandler.current = handler;

useEffect(() => {
return emitter.on(event, (data: T) => savedHandler.current(data));
}, [event]);
}

// 深层组件发送通知
function DeepChild() {
const handleClick = () => {
emitter.emit('notification', { message: '操作成功', type: 'success' });
};
return <button onClick={handleClick}>触发通知</button>;
}

// 顶层组件接收通知
function NotificationBar() {
const [msg, setMsg] = useState('');

useEvent('notification', useCallback((data: { message: string }) => {
setMsg(data.message);
}, []));

return msg ? <div className="notification">{msg}</div> : null;
}

方案 2:状态管理库(Zustand)

import { create } from 'zustand';

interface NotificationStore {
message: string | null;
show: (msg: string) => void;
clear: () => void;
}

const useNotification = create<NotificationStore>((set) => ({
message: null,
show: (msg) => set({ message: msg }),
clear: () => set({ message: null }),
}));

// 任意深层组件触发
function DeepChild() {
const show = useNotification((s) => s.show);
return <button onClick={() => show('操作成功')}>通知</button>;
}

// 任意位置响应
function NotificationBar() {
const message = useNotification((s) => s.message);
const clear = useNotification((s) => s.clear);

if (!message) return null;
return (
<div className="notification">
{message}
<button onClick={clear}>关闭</button>
</div>
);
}

方案 3:Ref 转发 + useImperativeHandle

// 适合命令式操作场景
interface ModalRef {
open: (title: string, content: string) => void;
close: () => void;
}

const GlobalModal = forwardRef<ModalRef>(function GlobalModal(_, ref) {
const [visible, setVisible] = useState(false);
const [title, setTitle] = useState('');
const [content, setContent] = useState('');

useImperativeHandle(ref, () => ({
open: (t: string, c: string) => {
setTitle(t);
setContent(c);
setVisible(true);
},
close: () => setVisible(false),
}));

if (!visible) return null;
return (
<div className="modal">
<h2>{title}</h2>
<p>{content}</p>
<button onClick={() => setVisible(false)}>关闭</button>
</div>
);
});

// 在顶层保存 ref
const modalRef = createRef<ModalRef>();

function App() {
return (
<>
<GlobalModal ref={modalRef} />
<PageContent />
</>
);
}

// 任意深层组件通过 ref 调用
function DeepChild() {
return (
<button onClick={() => modalRef.current?.open('提示', '操作成功')}>
打开弹窗
</button>
);
}

方案 4:URL 参数 / 搜索参数

import { useSearchParams } from 'react-router-dom';

// 组件 A:设置参数
function FilterPanel() {
const [, setSearchParams] = useSearchParams();

const handleFilter = (category: string) => {
setSearchParams({ category, page: '1' });
};

return (
<div>
<button onClick={() => handleFilter('electronics')}>电子产品</button>
<button onClick={() => handleFilter('clothing')}>服装</button>
</div>
);
}

// 组件 B:读取参数(可以在完全不同的层级)
function ProductList() {
const [searchParams] = useSearchParams();
const category = searchParams.get('category') ?? 'all';

// 根据 URL 参数加载数据
useEffect(() => {
fetchProducts(category);
}, [category]);

return <div>当前分类: {category}</div>;
}

方案对比表

方案优点缺点适用场景
发布订阅完全解耦、灵活难追踪数据流、易忘取消订阅全局通知、跨页面事件
Zustand/Redux可预测、DevTools引入额外依赖复杂业务状态
Ref 转发命令式、简单直接不够声明式、不易组合全局弹窗、Toast 等命令式 UI
URL 参数可分享、可书签只适合序列化数据筛选、分页、搜索条件
选择原则
  1. 优先使用 Props + 状态提升,这是 React 推荐的数据流
  2. 层级超过 3 层,考虑 Context 或状态管理库
  3. 完全无关的组件,用发布订阅或状态管理库
  4. 命令式 UI 操作(弹窗、Toast),用 Ref 或发布订阅
  5. 需要持久化/分享状态,用 URL 参数

相关链接