陌上人如玉
公子世无双

3.1 父子通信

3.1 父子通信

一句话: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 (
    
setForm((f) => ({ ...f, name: e.target.value }))} />
); }

三、兄弟通信:状态提升

// ❌ 兄弟组件无法直接通信(它们互相不认识)
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 {user.name};
}
// ✅ 方案一: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 {user.name};
}
// ✅ 方案二:状态管理库(适合「频繁变化 / 复杂逻辑 / 需要选择器订阅」)
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 ? {user.name} : 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)

八、面试延伸

  1. 「React 组件之间怎么通信?」
    按关系分:① 父传子用 props;② 子传父用「props 传回调」——子组件调用回调请求父组件更新;③ 兄弟用「状态提升」到公共父组件,一个改、一个读;④ 跨多层用 Context(低频数据)或状态管理库(高频数据、需要选择器);⑤ 非父子/全局用 Zustand/Redux,或(仅用于通知)事件总线。核心原则是「数据向下、事件向上」。

  2. 「为什么 React 是单向数据流?」
    因为单向数据流让「数据的来源与变化路径唯一且可追踪」:任何一个值只有「拥有它的那个组件」能改,其他组件只能读取或通过回调请求修改。这带来三个好处:① 调试时不需要追「谁改了它」(不存在多个写入点);② 状态与 UI 一致(不会出现「子组件改了但父组件不知道」);③ 易于实现「时间旅行/撤销重做」。如果允许双向绑定,数据流会变成网状的,复杂度爆炸。

  3. 「子组件想修改父组件的数据,该怎么做?」
    标准模式是「状态提升 + 回调下传」:① 把状态放在「需要它的最近公共父组件」;② 父组件把「修改函数」(如 setValue)或「语义化的回调」(如 onChange)作为 props 传给子组件;③ 子组件在合适的时机调用它。更推荐传「语义化的回调」(onSubmit / onSelect)而不是裸 setState,因为前者的耦合更小、子组件不需要知道父组件的状态结构。

  4. 「兄弟组件怎么通信?」
    两条路:① 状态提升——把共享状态放到它们的公共父组件里,父组件把「读的部分」和「写的回调」分别传给两个兄弟(一个改、一个读);② 全局状态/Context——如果两个兄弟的公共父组件层级太深、或者这个状态需要跨路由共享,就改用 Context 或状态管理库。React 里没有「兄弟直接引用」的机制,必须经过公共祖先或 external store。

  5. 「Context 和 props 怎么选?」
    判断标准是「层级深度 + 变化频率 + 使用方数量」:① 只透传 1~2 层 → 用 props(更显式、类型更清晰、没有额外的心智负担);② 透传 3 层以上且中间层完全不关心该数据 → 用 Context;③ 数据变化频繁 → 用带选择器订阅的状态库(因为 Context 的 value 一变会让所有消费者重渲染,性能代价大)。所以常见结论是「低频、跨多层用 Context;高频用状态库」。

  6. 「状态管理库和 Context 的区别?」
    Context 是「依赖注入 / 跨层级读取」的机制——它没有「选择器订阅、中间件、DevTools、时间旅行」这些能力,而且 value 变化会通知所有消费者;状态管理库(Zustand/Redux/Jotai)才是真正的「状态管理方案」,支持按选择器订阅(只订阅用到的那一部分),因此高频更新时性能更好。实践中两者经常结合:把 store 实例通过 Context 注入(避免全局单例),订阅与更新交给库。

  7. 「什么时候用事件总线?」
    只用于「解耦的通知类需求」——比如「全局的 Toast 提示」「埋点上报」「跨模块的一次性事件」。不适合用于「业务数据传递」,因为:① 数据流不可追踪(谁发的、谁收的不清楚);② 时序问题(发的时候可能没有订阅者);③ 容易忘记解绑导致泄漏与重复触发。业务数据应该走 props / Context / store。如果需要事件总线,务必在 useEffect 的清理函数里 off。

  8. 「自定义 Hook 能用来共享状态吗?」
    不能。自定义 Hook 复用的是「逻辑」,每个调用它的组件都有独立的状态(因为 state 挂在调用方的组件实例上)。比如 useToggle() 在 A、B 两个组件里各自独立。要共享状态必须使用:① Context(子树内共享);② 状态管理库(全局共享);③ 或者「状态提升到公共父组件」。这是自定义 Hook 最常见的误解之一。

一句话速记

React 是单向数据流:props 向下、回调向上;子改父 = 「状态提升 + 回调下传」(推荐传语义化回调如 onChange 而不是裸 setState);兄弟通信 = 状态提升到公共父组件(或用 Context/store);跨多层用 Context(低频)或状态管理库(高频、带选择器);自定义 Hook 不共享状态,只复用逻辑;事件总线只用于通知(且必须 off);props 传内联箭头函数会让 memo 失效。

赞(0) 打赏
未经允许不得转载:陌上寒 » 3.1 父子通信

评论 抢沙发

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

微信扫一扫

支付宝扫一扫