跳到主要内容

React 组件间通信方式

React 的核心思想之一是:单向数据流(Data Down, Actions Up)

  • 数据通常从父组件往下传(props)
  • 变化通常从子组件往上“通知”(回调/事件)

所谓“组件通信”,本质上就是:谁持有状态(state)?谁需要读/改它?我们用什么方式把“读”和“改”连接起来?


一、先分清 5 种通信场景(选型的关键)

场景典型关系首选方案什么时候升级方案
父 -> 子直接父子props / childrenprops 太多、层级太深时考虑 Context
子 -> 父直接父子回调函数(onXxx需要“命令式能力”时才考虑 ref
兄弟组件同一父组件下状态提升到最近公共父组件层级很深或共享范围变大时考虑 Context/状态库
跨多层级祖先 -> 深层Context(可配 useReducer需要全局共享、跨页面共享时考虑状态库
全局共享任意组件Redux / Zustand / Jotai 等状态复杂、多人协作、需要可追踪时优先 Redux 工具链
一句话记住

能用 props 就用 props;兄弟共享就“提升 state”;跨层但范围有限就用 Context;全局且复杂就上状态库。


二、父 -> 子:Props(最推荐、最清晰)

2.1 基本用法:把“数据”往下传

type User = { id: number; name: string };

function Parent() {
const user: User = { id: 1, name: 'Alice' };
return <UserCard user={user} />;
}

function UserCard({ user }: { user: User }) {
return <div>你好,{user.name}</div>;
}

2.2 组合(children)有时比“传一堆 props”更好

当你想让父组件决定布局/插槽内容时,推荐用组合:

import type { ReactNode } from 'react';

function Card({ title, children }: { title: string; children: ReactNode }) {
return (
<section>
<h2>{title}</h2>
<div>{children}</div>
</section>
);
}

function Page() {
return (
<Card title="个人信息">
<p>这里放任意内容</p>
</Card>
);
}

三、子 -> 父:回调函数(Actions Up)

子组件不要“偷偷改父组件的 state”,而是通过回调把意图告诉父组件

import { useState } from 'react';

function Parent() {
const [count, setCount] = useState(0);

return (
<>
<p>count: {count}</p>
<CounterButtons
onIncrement={() => setCount((c) => c + 1)}
onDecrement={() => setCount((c) => c - 1)}
/>
</>
);
}

function CounterButtons({
onIncrement,
onDecrement,
}: {
onIncrement: () => void;
onDecrement: () => void;
}) {
return (
<div>
<button onClick={onDecrement}>-1</button>
<button onClick={onIncrement}>+1</button>
</div>
);
}
命名小技巧

回调 props 用 onXxx(事件),子组件触发时用 handleXxx(处理)。


四、兄弟组件:状态提升(Lift State Up)

兄弟之间不要互相“直接调用”,更推荐把共享状态放到最近公共父组件:

import { useState } from 'react';

function Parent() {
const [keyword, setKeyword] = useState('');

return (
<>
<SearchInput value={keyword} onChange={setKeyword} />
<SearchPreview keyword={keyword} />
</>
);
}

function SearchInput({
value,
onChange,
}: {
value: string;
onChange: (next: string) => void;
}) {
return (
<input
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder="输入关键词"
/>
);
}

function SearchPreview({ keyword }: { keyword: string }) {
return <div>你正在搜索:{keyword || '(空)'}</div>;
}

这个模式的好处是:

  • 数据来源清晰:keyword 只有一份
  • 组件更纯:兄弟组件只关心自己的输入/展示

五、跨层级:Context(避免“层层传 props”)

当组件树很深、但共享范围局部且稳定时,Context 很合适。

5.1 最小可用示例

import { createContext, useContext } from 'react';

type Theme = 'light' | 'dark';

const ThemeContext = createContext<Theme>('light');

function App() {
return (
<ThemeContext.Provider value="dark">
<Layout />
</ThemeContext.Provider>
);
}

function Layout() {
return <DeepChild />;
}

function DeepChild() {
const theme = useContext(ThemeContext);
return <div>当前主题:{theme}</div>;
}

5.2 性能提醒:Context 会影响“订阅它的组件”

Context 的 value 变了,所有 useContext 订阅者都会重新渲染。

常见优化思路:

  • 拆分 Context(把经常变的和不常变的分开)
  • value 尽量稳定(避免每次 render 都创建新对象)
  • 对复杂场景使用带 selector 的状态库(例如 Zustand/Redux)

六、Context + useReducer:中等规模的“局部全局状态”

当你需要在一片页面/模块内共享“状态 + 更新逻辑”,可以用 useReducer 管理,再用 Context 分发。

import { createContext, useContext, useMemo, useReducer } from 'react';
import type { Dispatch, ReactNode } from 'react';

type State = { count: number };
type Action = { type: 'inc' } | { type: 'dec' };

const CounterContext = createContext<
{ state: State; dispatch: Dispatch<Action> } | undefined
>(undefined);

function reducer(state: State, action: Action): State {
switch (action.type) {
case 'inc':
return { count: state.count + 1 };
case 'dec':
return { count: state.count - 1 };
default:
return state;
}
}

function CounterProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(reducer, { count: 0 });
const value = useMemo(() => ({ state, dispatch }), [state]);

return <CounterContext.Provider value={value}>{children}</CounterContext.Provider>;
}

function useCounter() {
const ctx = useContext(CounterContext);
if (!ctx) throw new Error('useCounter 必须在 CounterProvider 内使用');
return ctx;
}

function CounterPanel() {
const { state, dispatch } = useCounter();
return (
<div>
<p>count: {state.count}</p>
<button onClick={() => dispatch({ type: 'dec' })}>-1</button>
<button onClick={() => dispatch({ type: 'inc' })}>+1</button>
</div>
);
}

七、命令式通信:ref / forwardRef / useImperativeHandle(少用但有用)

React 更推崇声明式:通过 state 描述 UI。但有些场景确实需要“命令式动作”(例如:聚焦 input、滚动到某处、播放/暂停视频)。

import { forwardRef, useImperativeHandle, useRef } from 'react';

type InputHandle = {
focus: () => void;
clear: () => void;
};

const FancyInput = forwardRef<InputHandle, { placeholder?: string }>(
({ placeholder }, ref) => {
const inputRef = useRef<HTMLInputElement>(null);

useImperativeHandle(ref, () => ({
focus() {
inputRef.current?.focus();
},
clear() {
if (inputRef.current) inputRef.current.value = '';
},
}));

return <input ref={inputRef} placeholder={placeholder} />;
}
);

function Page() {
const ref = useRef<InputHandle>(null);

return (
<>
<FancyInput ref={ref} placeholder="请输入" />
<button onClick={() => ref.current?.focus()}>聚焦</button>
<button onClick={() => ref.current?.clear()}>清空</button>
</>
);
}
使用边界

ref 更像“后门”。只在必须操作 DOM 或第三方实例时使用,避免把业务数据流做成命令式调用。


八、事件总线(Event Bus):能用但不推荐

事件总线看起来“谁都能发、谁都能收”,但缺点也明显:

  • 很难追踪:谁在什么时候发了什么事件?
  • 很难维护:事件名像字符串魔法,改动容易漏
  • 容易产生“隐式依赖”和内存泄漏(忘记取消订阅)

如果你只是为了“跨层通知”,优先考虑 Context / 状态库

下面是一个用浏览器 EventTarget 做的最小示例(仅用于理解):

type User = { id: number; name: string };

const bus = new EventTarget();

export function emitUserUpdated(user: User) {
bus.dispatchEvent(new CustomEvent<User>('user:updated', { detail: user }));
}

export function onUserUpdated(listener: (user: User) => void) {
const handler = (e: Event) => listener((e as CustomEvent<User>).detail);
bus.addEventListener('user:updated', handler);
return () => bus.removeEventListener('user:updated', handler);
}

九、全局共享:状态管理库(Redux / Zustand / Jotai ...)

当状态需要跨很多页面/模块共享,或者多人协作需要更强的可追踪性,建议引入状态库。

9.1 用 Zustand 举个最小例子(上手快)

pnpm add zustand
import { create } from 'zustand';

type Store = {
count: number;
inc: () => void;
dec: () => void;
};

const useCounterStore = create<Store>((set) => ({
count: 0,
inc: () => set((s) => ({ count: s.count + 1 })),
dec: () => set((s) => ({ count: s.count - 1 })),
}));

function A() {
const count = useCounterStore((s) => s.count);
return <div>A: {count}</div>;
}

function B() {
const inc = useCounterStore((s) => s.inc);
return <button onClick={inc}>+1</button>;
}
什么时候选 Redux?

当你需要更完整的工程化能力(规范、可追踪、调试、多人协作约束)时,Redux Toolkit 往往更稳。


十、“服务端状态”也是一种通信:SWR / React Query 的缓存共享

很多时候你以为自己在做“组件通信”,其实是在做服务端数据共享

  • Header 和 Profile 都要展示当前用户
  • 只要它们用同一个 key/query,就能共享缓存与更新

可以配合阅读:SWR:优雅的数据请求


十一、常见坑与最佳实践

  1. 避免“prop drilling”失控:层级太深就考虑 Context(或拆组件、用组合模式)。
  2. 不要把所有东西都塞进 Context:频繁变化的大对象会导致大量无谓渲染;必要时拆分 Context 或用状态库 selector。
  3. 保持数据流可追踪:优先“显式的 props + 回调”,比“隐式的事件总线”更好维护。
  4. 状态放在“最接近使用它的地方”:既不要过高(全局污染),也不要过低(兄弟不同步)。
  5. 区分两类状态:UI 状态(弹窗开关、当前 tab)与服务端状态(请求数据)不要混在一个 store 里。

十二、快速选型:我到底该用哪个?


十三、面试高频问答

1)React 组件通信的核心原则是什么?

  • 单向数据流:数据向下(props),动作向上(回调);共享状态要有唯一来源(single source of truth)。

2)父传子有哪些方式?

  • 主要是 props;还包括 children/组合模式(父决定结构,子只负责渲染插槽)。

3)子传父怎么做?为什么不用“直接改父组件的 state”?

  • 用回调(onXxx)。子组件不应该持有父组件 state 的修改权,否则数据流会变得不可控、难维护。

4)兄弟组件怎么通信?

  • 把共享状态提升到最近公共父组件,由父组件统一把数据/回调传下去。

5)什么情况下用 Context?它解决什么问题?

  • 用在“跨多层级、共享范围有限”的场景,解决层层传 props(prop drilling)的问题。

6)Context 的性能问题是什么?怎么缓解?

  • value 变化会触发所有订阅者重渲染。缓解:拆分 Context、让 value 稳定(useMemo)、或用 selector 型状态库。

7)什么时候会用 ref 做通信?有什么风险?

  • 需要命令式操作(聚焦、滚动、控制第三方实例)时。风险:破坏声明式数据流,业务逻辑容易变成“到处调用方法”。

8)事件总线为什么不推荐?

  • 依赖隐式、难追踪、难重构、容易泄漏;更推荐显式数据流(props/Context/状态库)。

9)为什么说“服务端状态库”也是通信?

  • 多组件共享同一份远端数据时,本质是共享缓存与更新;SWR/React Query 通过 key/query 实现跨组件同步。

10)如果让我在项目里做选型,你的优先级是什么?

  • props/回调提升 stateContext(局部共享)状态库(全局共享),并优先保证数据流清晰可追踪。