以一个"修改用户名"的表单为例,讲清楚 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} />;
};
这有什么不好?
- 父组件被迫 import
React.ChangeEvent<HTMLInputElement>,但这个事件类型只跟<input>有关,理论上父不应该关心 - 父的
handleChange要自己从e.target.value提取值,这是"脏活" - 如果子组件以后换成
<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 在哪 | 父 | 子 | 父 |
| 每次按键通知父? | ✅ | ❌ | ✅ |
| 父能感知编辑过程? | ✅ | ❌ | ✅ |
| 每次按键重渲染范围 | 父+所有子 | 只有子 | 父+所有子 |
| 适用场景 | 不推荐 | 父只关心最终结果 | 父需实时感知(校验/联动/预览) |
实战建议
- 先考虑写法二(自治)------把 state 放在真正用它的最小组件里,性能最好,封装最干净
- 当父组件确实需要"编辑中"的值时------才用写法三,把 state 提升上去
- 写法一基本不用------它是写法三的前身,直接跳到三就好
和 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,示例代码来自本人的学习项目。