从困惑到理解:React 父子组件通信的三种写法

以一个"修改用户名"的表单为例,讲清楚 React 里最基础也最容易搞混的父子组件通信。


先抛结论

React 里只有一种通信方向:

复制代码
父组件 ──props──→ 子组件(数据往下走)
子组件 ──调用回调──→ 父组件(通知往上走)

Props 不是双向管道。 子组件永远不能直接改父组件的 state。所谓的"子传父",本质是父把自己的函数通过 props 提前给了子,子需要时调用这个函数。

打个比方:父把自家钥匙交给子,子开门用的还是父的钥匙、改的还是父的门。钥匙(函数)是父给的,门(state)也一直是父的。


从一个表单需求开始

需求很简单:页面上显示一个名字,下面有一个编辑区,修改后点确认更新名字。

ini 复制代码
┌─────────────────────────┐
│  hello 张三              │  ← 展示当前名字
└─────────────────────────┘

┌─────────────────────────┐
│  Update name: [____]    │  ← 编辑区
│  [Submit]               │
└─────────────────────────┘

这个需求有三种写法,依次是大多数人的学习路径。


写法一:传原始事件对象(能用,但不优雅)

刚开始学 React 时,很容易写成这样------子组件把 onChange 传上来的事件对象原封不动地交给父组件:

tsx 复制代码
// 子组件
interface Props {
    username: string;
    onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}

const NameEditComponent: React.FC<Props> = (props) => {
    return <input value={props.username} onChange={props.onChange} />;
};

// 父组件
const App = () => {
    const [name, setName] = useState('张三');

    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        setName(e.target.value);  // 父必须自己从 event 对象里掏值
    };

    return <NameEditComponent username={name} onChange={handleChange} />;
};

这有什么不好?

  1. 父组件被迫 import React.ChangeEvent<HTMLInputElement>,但这个事件类型只跟 <input> 有关,理论上父不应该关心
  2. 父的 handleChange 要自己从 e.target.value 提取值,这是"脏活"
  3. 如果子组件以后换成 <textarea>,父组件的类型也要改

一句话评价:能用,但子组件把复杂度泄露给了父组件。


写法二:子组件自己持有状态(自治)

既然中间编辑过程父组件不需要知道,那不如让子组件自己管理?

tsx 复制代码
// 子组件
interface Props {
    initialUserName: string;                          // 初始值,只读一次
    onNameUpdated: (newName: string) => void;         // 提交时才通知父
}

const NameEditComponent: React.FC<Props> = (props) => {
    const [editingName, setEditingName] = useState(props.initialUserName);

    const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        setEditingName(e.target.value);  // 自己改自己的 state
    };

    const onNameSubmit = () => {
        props.onNameUpdated(editingName);  // 点按钮才告诉父
    };

    return (
        <>
            <label>Update name:</label>
            <input value={editingName} onChange={onChange} />
            <button onClick={onNameSubmit}>Change</button>
        </>
    );
};

// 父组件
const App = () => {
    const [name, setName] = useState('张三');

    return (
        <>
            名字:{name}
            <NameEditComponent
                initialUserName={name}
                onNameUpdated={(newName) => setName(newName)}
            />
        </>
    );
};

好在哪?

  • 每次按键只重渲染子组件,父组件完全不受影响------性能更好
  • 父组件不关心中间的编辑过程,只关心最终结果
  • 子组件完全封装了自己的逻辑,是一个独立的小模块

有什么局限?

  • 父组件拿不到"编辑中的值"------如果想做实时预览、实时校验,就做不到了
  • 如果另一个子组件也要用这个编辑中的值,也拿不到

写法三:状态提升(受控组件)

如果父组件需要实时知道编辑中的值------比如要显示编辑内容的长度、要判断按钮是否禁用、要做实时预览------就该把 state 从子组件"提升"到父组件:

tsx 复制代码
// 子组件 ------ 完全无状态,只负责展示 + 转发事件
interface Props {
    editingName: string;
    onEditingNameUpdated: (newEditingName: string) => void;
    onNameUpdated: () => void;
    disable: boolean;
}

const NameEditComponent: React.FC<Props> = (props) => {
    const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        onEditingNameUpdated(e.target.value);  // 提取好值,父只拿到干净的 string
    };

    return (
        <>
            <label>Update name:</label>
            <input value={editingName} onChange={onChange} />
            <button disabled={disable} onClick={onNameUpdated}>Submit</button>
        </>
    );
};

// 父组件 ------ 持有所有状态
const App = () => {
    const [name, setName] = useState('张三');
    const [editingName, setEditingName] = useState('张三');

    return (
        <>
            名字:{name}
            <NameEditComponent
                editingName={editingName}
                onEditingNameUpdated={setEditingName}   // 直接传 setState
                onNameUpdated={() => setName(editingName)}
                disable={editingName === '' || editingName === name}
            />
        </>
    );
};

状态提升的本质

就是把 useState 从子组件剪切、粘贴 到父组件。子组件从 UI = f(state, props) 变成了纯粹的 UI = fn(props)------职责极度单一,只负责展示。

每次按键,到底发生了什么?

这是大多数初学者最困惑的地方。你以为你直接在改 <input>,实际流程是这样的:

lua 复制代码
① 你在 <input> 中按 "L"
     ↓
② 浏览器触发 onChange
   子调用 props.onEditingNameUpdated("张三L")
   (子只是传话,自己不改任何东西)
     ↓
③ 父的 setEditingName("张三L") 执行
   state 从 "张三" 变成 "张三L"
     ↓
④ 父重新渲染
   <NameEditComponent editingName="张三L" ... />
                         ↑ 新值通过 props 再次传给子
     ↓
⑤ 子收到新 props → 重渲染
   <input value="张三L" />
   浏览器 DOM 终于更新,你看到了 "张三L"

这就是"受控"的含义------input 的 value 被 React props 完全接管。浏览器自己的输入行为被劫持了:如果不通过 onChange → setState 这条链路更新 props,输入框的值永远不会变。


三种写法,什么时候用哪个?

写法一(传事件对象) 写法二(自治) 写法三(状态提升)
State 在哪
每次按键通知父?
父能感知编辑过程?
每次按键重渲染范围 父+所有子 只有子 父+所有子
适用场景 不推荐 父只关心最终结果 父需实时感知(校验/联动/预览)

实战建议

  1. 先考虑写法二(自治)------把 state 放在真正用它的最小组件里,性能最好,封装最干净
  2. 当父组件确实需要"编辑中"的值时------才用写法三,把 state 提升上去
  3. 写法一基本不用------它是写法三的前身,直接跳到三就好

和 TypeScript 的关系

用 TypeScript 写 React 时,Props 的类型定义本身就在告诉所有开发者"这个组件的契约是什么":

ts 复制代码
// interface 定义组件的契约
interface Props {
    editingName: string;                              // 我需要这些数据
    onNameUpdated: () => void;                        // 我会在什么时候通知你
    disable: boolean;
}

React.FC<Props> 这个泛型保证了你的组件必须满足这个契约------少传了属性、传错了类型,TS 直接在编辑器里标红,不需要等到运行时。


小结

核心概念 一句话
单向数据流 数据只从父→子,通过 props
子通知父 调用父通过 props 传下来的回调函数
状态提升 把 useState 从子搬到父
受控组件 value 被 props 控制,子只展示
自治组件 子有自己的 state,提交时才通知

记住这张图就够了:

perl 复制代码
父组件
  │
  │  props(数据 + 回调函数)
  ▼
子组件
  │
  │  调用 props.onXxx()
  └──── 等于调用父的函数 ────→ 父改 state → 父重渲染 → 新 props 回来

本文基于 React 19 + TypeScript,示例代码来自本人的学习项目。

相关推荐
阿黎梨梨1 小时前
React + TypeScript 入门:从组件类型到数据流设计
react.js
烬羽1 小时前
React 状态归属:两个版本的用户名编辑器,告诉你 state 该放哪
react.js·typescript·前端框架
名字还没想好☜2 小时前
React createPortal 实战:模态框逃出 overflow:hidden、事件冒泡与焦点管理
前端·javascript·react.js·ecmascript·react·createportal
breeze jiang2 小时前
React + TypeScript 编辑表单:为什么要区分 name 和 editingName
前端·typescript
gis开发之家18 小时前
《Vue3 从入门到大神40篇》Vue3 源码详解(十):diff 算法全解析 —— 为什么 Vue3 比 Vue2 更快?
javascript·算法·typescript·前端框架·vue3·vue3源码
GitLqr20 小时前
提升 Web 开发效率:我常用的 10 个 TypeScript 实战技巧
vue.js·react.js·typescript
勇往直前plus1 天前
Vue3(篇四)单页面应用到路由管理
javascript·typescript·前端框架·vue
勇往直前plus1 天前
Vue3(篇三) Element Plus
前端·javascript·typescript·vue
无人生还1 天前
从 Vue3 到 React · 快速上手系列第 6 篇:状态管理 useState 与 useReducer
前端·vue.js·react.js