React 组件间通信方式
React 的核心思想之一是:单向数据流(Data Down, Actions Up)。
- 数据通常从父组件往下传(props)
- 变化通常从子组件往上“通知”(回调/事件)
所谓“组件通信”,本质上就是:谁持有状态(state)?谁需要读/改它?我们用什么方式把“读”和“改”连接起来?
一、先分清 5 种通信场景(选型的关键)
| 场景 | 典型关系 | 首选方案 | 什么时候升级方案 |
|---|---|---|---|
| 父 -> 子 | 直接父子 | props / children | props 太多、层级太深时考虑 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 Toolkit 往往更稳。
十、“服务端状态”也是一种通信:SWR / React Query 的缓存共享
很多时候你以为自己在做“组件通信”,其实是在做服务端数据共享:
- Header 和 Profile 都要展示当前用户
- 只要它们用同一个 key/query,就能共享缓存与更新
可以配合阅读:SWR:优雅的数据请求。
十一、常见坑与最佳实践
- 避免“prop drilling”失控:层级太深就考虑 Context(或拆组件、用组合模式)。
- 不要把所有东西都塞进 Context:频繁变化的大对象会导致大量无谓渲染;必要时拆分 Context 或用状态库 selector。
- 保持数据流可追踪:优先“显式的 props + 回调”,比“隐式的事件总线”更好维护。
- 状态放在“最接近使用它的地方”:既不要过高(全局污染),也不要过低(兄弟不同步)。
- 区分两类状态: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/回调 → 提升 state → Context(局部共享) → 状态库(全局共享),并优先保证数据流清晰可追踪。