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 转发通信

通过 refuseImperativeHandle,父组件可以直接调用子组件暴露的方法。

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 组件通信方式的选择应遵循以下原则:

  1. 能用 props 解决就用 props,保持单向数据流
  2. 就近原则:状态应放在需要它的最近公共祖先
  3. Context 用于低频更新的全局数据(主题、国际化、用户信息)
  4. 高频更新的全局状态使用专门的状态管理库
  5. 避免滥用事件总线,它会让数据流变得不可预测
  6. ref 通信用于命令式操作,不要用来替代声明式数据流