
一句话:React 的数据流是单向的——数据通过 props 向下传递,子组件通过调用父组件传来的回调向上通知;兄弟组件之间靠「把状态提升到公共父组件」通信。
一、父传子:props
// 父组件
function Parent() {
const user = { name: '张三', age: 18 };
return ;
}
// 子组件
function Child({ user, title, count, isActive }: ChildProps) {
return (
{title}
{user.name} / {user.age}
{count} / {String(isActive)}
);
}
传递形式速查:
→ 字符串字面量
→ 表达式(数字、变量、对象、数组、函数)
→ 布尔 true(不写值即 true)
→ 对象字面量(注意是双层大括号)
{}}> → 函数
→ 展开传递
{children} → 嵌套内容变成 children
二、子传父:回调
// 父组件:把「修改状态的方法」传下去
function Parent() {
const [keyword, setKeyword] = useState('');
return (
<>
搜索:{keyword}
>
);
}
// 子组件:只负责「通知」,不关心父组件怎么处理
function SearchInput({ value, onChange }: { value: string; onChange: (v: string) => void }) {
return (
onChange(e.target.value)} // ⭐ 调用回调,把新值「交给」父组件
/>
);
}
三种常见的回调命名约定:
| 命名 | 语义 | 说明 |
|---|---|---|
onXxx |
「发生了某件事」 | <Comp onChange={...} />(事件式) |
onXxxChange |
带值的变更 | React 生态习惯 |
setXxx |
直接传 setter | <Comp value={v} setValue={setV} />(简洁但耦合更强) |
// 更复杂的场景:子组件需要传多个值
function Form({ onSubmit }: { onSubmit: (data: FormData) => void }) {
const [form, setForm] = useState({ name: '', age: 0 });
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSubmit(form); // ⭐ 一次性把数据交给父组件
};
return (
);
}
三、兄弟通信:状态提升
// ❌ 兄弟组件无法直接通信(它们互相不认识)
function Parent() {
return (
<>
{/* 想让 A 的数据影响 B */}
>
);
}
// ✅ 把共享状态「提升」到公共父组件
function Parent2() {
const [selected, setSelected] = useState(null); // ⭐ 状态在公共父组件
return (
<>
{/* 一个改 */}
{/* 一个读 */}
>
);
}
function SiblingA({ selected, onSelect }: { selected: string | null; onSelect: (v: string) => void }) {
return ;
}
function SiblingB({ selected }: { selected: string | null }) {
return 当前选中:{selected ?? '无'};
}
四、跨层级:Context 与状态库
// ❌ props 层层透传(props drilling)
function App({ user }) {
return ;
}
function Layout({ user }) {
return ; // Layout 完全用不到 user
}
function Sidebar({ user }) {
return ;
}
function Avatar({ user }) {
return
;
}
// ✅ 方案一:Context(适合「不常变化的共享值」)
const UserContext = createContext(null);
export function useUser() {
const ctx = useContext(UserContext);
if (!ctx) throw new Error('useUser 必须在 UserProvider 内使用');
return ctx;
}
function App2() {
const user = useCurrentUser();
return (
);
}
function Avatar2() {
const user = useUser(); // ⭐ 需要时自己取,中间层不用管
return
;
}
// ✅ 方案二:状态管理库(适合「频繁变化 / 复杂逻辑 / 需要选择器订阅」)
import { create } from 'zustand';
const useUserStore = create<{
user: User | null;
setUser: (u: User | null) => void;
}>((set) => ({
user: null,
setUser: (user) => set({ user }),
}));
function Avatar3() {
const user = useUserStore((s) => s.user); // ⭐ 选择器:只在 user 变化时重渲染
return user ?
: null;
}
| 方案 | 适合 | 代价 |
|---|---|---|
| props 透传 1~2 层 | 大多数场景 | 显式、类型清晰,但层数多了啰嗦 |
| Context | 跨多层、低频变化(主题/用户/语言/权限) | value 变化会让所有消费者重渲染 |
| 状态管理库 | 高频变化、复杂逻辑、需要选择器 | 引入依赖与一定心智负担 |
| 事件总线 | 解耦的「通知」(如全局 toast) | 难以追踪数据流,不推荐用于业务数据 |
五、非父子关系:四种方案
// ① Context —— 同一个「组件子树」内的通信(最推荐)
// ② 状态管理库 —— 跨路由、跨页面的共享(Zustand/Redux)
// ③ Portal + Context —— 弹层、Toast(渲染位置在别处,但仍属于组件子树)
// ④ 事件总线(mitt)—— 只用于「通知」类需求(谨慎)
import mitt from 'mitt';
const bus = mitt<{ 'user:login': User; 'toast:show': string }>();
// 发送方
bus.emit('toast:show', '保存成功');
// 接收方(⭐ 必须在卸载时取消订阅)
useEffect(() => {
const handler = (msg: string) => showToast(msg);
bus.on('toast:show', handler);
return () => bus.off('toast:show', handler);
}, []);
六、方案选择速查
| 关系 | 推荐方案 |
|---|---|
| 父 → 子(数据) | props |
| 子 → 父(通知) | props 传回调 |
| 子 → 父(数据 + 校验) | 受控组件的 value + onChange |
| 父 → 子(定制渲染) | children 或 render props(传元素 props) |
| 父 → 子(命令式操作) | ref + useImperativeHandle |
| 兄弟 | 状态提升到公共父组件 |
| 跨 2~3 层 | 继续 props,或用 Context |
| 跨多层(低频数据) | Context |
| 跨多层(高频数据) | 状态管理库(Zustand 等,带选择器) |
| 全局通知(Toast/埋点) | Context 或事件总线 |
| 跨标签页 | BroadcastChannel / storage 事件 |
七、常见坑
// ① 直接修改 props(破坏单向数据流)
function Child({ items }) {
items.push('x'); // ❌
}
// ② 把 props 复制进 state(后续不同步)
function Child2({ initialValue }) {
const [value, setValue] = useState(initialValue); // ⚠️ props 变化不会同步
}
// ✅ 需要「可就地编辑」时用 key 强制重建,或用受控模式
;
// ③ 传内联箭头函数导致子组件的 memo 失效
doSomething(id)} /> // ⚠️ 每次渲染都是新函数
// ✅ 稳定引用或用 data-* + 事件委托
const handleClick = useCallback((id: string) => doSomething(id), []);
// ④ 在子组件里「回调 + setState」形成循环
function Child3({ value, onChange }) {
useEffect(() => {
onChange(value + 1); // ❌ 渲染 → onChange → 父组件更新 → 再渲染
}, [value, onChange]);
}
// ⑤ props 透传三层以上还硬传(应该用 Context)
// ⑥ 用 Context 传「高频变化的值」(导致全量重渲染)
// ⑦ 事件总线忘记 off → 内存泄漏 + 重复触发
// ⑧ 在子组件里存「父组件数据的副本」→ 数据源不唯一
// ⑨ 兄弟组件之间试图直接互相引用(做不到,必须走父组件或 store)
// ⑩ 用 ref 传数据(反模式,见 2.5)
八、面试延伸
- 「React 组件之间怎么通信?」
按关系分:① 父传子用props;② 子传父用「props 传回调」——子组件调用回调请求父组件更新;③ 兄弟用「状态提升」到公共父组件,一个改、一个读;④ 跨多层用Context(低频数据)或状态管理库(高频数据、需要选择器);⑤ 非父子/全局用 Zustand/Redux,或(仅用于通知)事件总线。核心原则是「数据向下、事件向上」。 -
「为什么 React 是单向数据流?」
因为单向数据流让「数据的来源与变化路径唯一且可追踪」:任何一个值只有「拥有它的那个组件」能改,其他组件只能读取或通过回调请求修改。这带来三个好处:① 调试时不需要追「谁改了它」(不存在多个写入点);② 状态与 UI 一致(不会出现「子组件改了但父组件不知道」);③ 易于实现「时间旅行/撤销重做」。如果允许双向绑定,数据流会变成网状的,复杂度爆炸。 -
「子组件想修改父组件的数据,该怎么做?」
标准模式是「状态提升 + 回调下传」:① 把状态放在「需要它的最近公共父组件」;② 父组件把「修改函数」(如setValue)或「语义化的回调」(如onChange)作为 props 传给子组件;③ 子组件在合适的时机调用它。更推荐传「语义化的回调」(onSubmit/onSelect)而不是裸setState,因为前者的耦合更小、子组件不需要知道父组件的状态结构。 -
「兄弟组件怎么通信?」
两条路:① 状态提升——把共享状态放到它们的公共父组件里,父组件把「读的部分」和「写的回调」分别传给两个兄弟(一个改、一个读);② 全局状态/Context——如果两个兄弟的公共父组件层级太深、或者这个状态需要跨路由共享,就改用 Context 或状态管理库。React 里没有「兄弟直接引用」的机制,必须经过公共祖先或 external store。 -
「Context 和 props 怎么选?」
判断标准是「层级深度 + 变化频率 + 使用方数量」:① 只透传 1~2 层 → 用 props(更显式、类型更清晰、没有额外的心智负担);② 透传 3 层以上且中间层完全不关心该数据 → 用 Context;③ 数据变化频繁 → 用带选择器订阅的状态库(因为 Context 的 value 一变会让所有消费者重渲染,性能代价大)。所以常见结论是「低频、跨多层用 Context;高频用状态库」。 -
「状态管理库和 Context 的区别?」
Context 是「依赖注入 / 跨层级读取」的机制——它没有「选择器订阅、中间件、DevTools、时间旅行」这些能力,而且 value 变化会通知所有消费者;状态管理库(Zustand/Redux/Jotai)才是真正的「状态管理方案」,支持按选择器订阅(只订阅用到的那一部分),因此高频更新时性能更好。实践中两者经常结合:把 store 实例通过 Context 注入(避免全局单例),订阅与更新交给库。 -
「什么时候用事件总线?」
只用于「解耦的通知类需求」——比如「全局的 Toast 提示」「埋点上报」「跨模块的一次性事件」。不适合用于「业务数据传递」,因为:① 数据流不可追踪(谁发的、谁收的不清楚);② 时序问题(发的时候可能没有订阅者);③ 容易忘记解绑导致泄漏与重复触发。业务数据应该走 props / Context / store。如果需要事件总线,务必在useEffect的清理函数里off。 -
「自定义 Hook 能用来共享状态吗?」
不能。自定义 Hook 复用的是「逻辑」,每个调用它的组件都有独立的状态(因为 state 挂在调用方的组件实例上)。比如useToggle()在 A、B 两个组件里各自独立。要共享状态必须使用:① Context(子树内共享);② 状态管理库(全局共享);③ 或者「状态提升到公共父组件」。这是自定义 Hook 最常见的误解之一。
一句话速记
React 是单向数据流:props 向下、回调向上;子改父 = 「状态提升 + 回调下传」(推荐传语义化回调如
onChange而不是裸setState);兄弟通信 = 状态提升到公共父组件(或用 Context/store);跨多层用 Context(低频)或状态管理库(高频、带选择器);自定义 Hook 不共享状态,只复用逻辑;事件总线只用于通知(且必须off);props传内联箭头函数会让memo失效。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!