摘要:React 组件之间怎么传数据?父传子靠 props,子传父靠回调。本文用一套完整的 TodoList 项目------App 管状态、TodoInput 上报新增、TodoList 展示与操作、TodoStates 底部统计------把四种通信模式串成一条完整的组件树,外加三个增删改操作的数据流拆解。
目录
- 组件树:先规划结构,再动手写代码
- [父传子:props 让数据向下流](#父传子:props 让数据向下流 "#%E4%BA%8C%E7%88%B6%E4%BC%A0%E5%AD%90props-%E8%AE%A9%E6%95%B0%E6%8D%AE%E5%90%91%E4%B8%8B%E6%B5%81")
- 子传父:回调函数让事件向上冒
- 三个核心操作的数据流拆解
- 总结
一、组件树:先规划结构,再动手写代码
在写第一行 JSX 之前,先画出组件树。一套 TodoList 应用可以拆成四个组件:
| 组件 | 职责 | 持有的状态 | 从父组件接收 |
|---|---|---|---|
| App | 所有 todo 的增删改查 | todos 数组 |
--- |
| TodoInput | 输入框 + 提交 | 本地的 inputValue |
onAdd 回调 |
| TodoList | 渲染列表、勾选、删除 | --- | todos, onToggle, onDelete |
| TodoStates | 总数/活跃/完成统计、清空 | --- | total, active, completed, onClearCompleted |
核心设计原则:数据状态只在一个地方------App 组件。 所有子组件通过 props 读取数据,通过回调函数请求修改。这就是所谓的"单一数据源(Single Source of Truth)"------数据不出问题,因为修改数据的入口只有一个。
二、父传子:props 让数据向下流
App 持有 todos 数组,子组件需要的数据全部通过 props 传递:
jsx
// App.jsx --- 父组件
const [todos, setTodos] = useState([
{ id: 1, text: '吃饭', completed: false },
{ id: 2, text: '睡觉', completed: false },
{ id: 3, text: '打豆豆', completed: false },
]);
const activeCount = todos.filter(t => !t.completed).length;
const completedCount = todos.length - activeCount;
return (
<div>
<TodoInput onAdd={addTodo} />
<TodoList
todos={todos}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
<TodoStates
total={todos.length}
active={activeCount}
completed={completedCount}
onClearCompleted={clearCompleted}
/>
</div>
);
注意 activeCount 和 completedCount 不是 useState 声明的------它们是计算值(computed value) ,直接从 todos 派生。每次 todos 变化时,组件重渲染,计算值自动重新得出。不需要手动同步,不需要 useEffect 去监听。
三、子传父:回调函数让事件向上冒
子组件不能直接修改父组件的状态 ------这是 React 单向数据流的核心约束。那子组件怎么告诉父组件"用户点了我"?通过 props 传入的回调函数。本质是"自定义事件"的 React 版本。
TodoInput:上报新增
jsx
const TodoInput = ({ onAdd }) => {
const [inputValue, setInputValue] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
if (inputValue.trim() === '') return;
onAdd(inputValue); // 通知父组件:用户输入了 text
setInputValue(''); // 清空本地输入框
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={inputValue}
onChange={e => setInputValue(e.target.value)}
placeholder="what needs to be done?"
autoFocus
/>
<button type="submit">Add</button>
</form>
);
};
inputValue 是 TodoInput 自己的 本地状态------它只和输入框的 UI 有关,不需要上升到 App。onAdd 是 App 传来的回调,调用它等于通知父组件"该新增一条 todo 了"。这是一种职责分离------TodoInput 只管收集用户输入和提交时机,不管数据怎么存储;App 只管数据怎么存储,不管输入框长什么样。
TodoList:上报操作
jsx
const TodoList = ({ todos, onToggle, onDelete }) => {
return (
<ul className="todo-list">
{todos.length === 0 ? (
<li className="empty">No todos yet!</li>
) : (
todos.map(todo => (
<li key={todo.id} className={todo.completed ? 'completed' : ''}>
<label>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span>{todo.text}</span>
</label>
<button onClick={() => onDelete(todo.id)}>删除</button>
</li>
))
)}
</ul>
);
};
TodoList 不持有任何自己的状态------它全凭 props 渲染。勾选复选框 → 调 onToggle(id);点击删除 → 调 onDelete(id)。它不知道也不关心这些回调在 App 里具体怎么实现,只要"通知"出去就完成了职责。
TodoStates:上报清理
jsx
const TodoStates = ({ total, active, completed, onClearCompleted }) => {
return (
<div className="todo-states">
<p>Total: {total} | Active: {active} | Completed: {completed}</p>
{completed > 0 && (
<button onClick={onClearCompleted} className="clear-btn">
Clear Completed
</button>
)}
</div>
);
};
completed > 0 && ------ 没有已完成项时按钮不出现。onClearCompleted 不传参数,App 自己知道怎么清(filter 掉所有 completed 为 true 的条目)。
四、三个核心操作的数据流拆解
添加 todo
jsx
const addTodo = (text) => {
setTodos([
{ id: Date.now(), text, completed: false },
...todos
]);
};
Date.now() 做临时 id(生产环境应使用 crypto.randomUUID()),新对象放在数组开头------新加的 todo 显示在最上面。
切换完成状态
jsx
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
));
};
{ ...todo, completed: !todo.completed } ------ 用展开运算符复制原对象,只翻转 completed 字段。这是不可变更新的标准写法:不修改原对象,返回一个新对象。
删除 todo
jsx
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
filter 本身就是纯函数------返回新数组,不修改原数组。只需一行代码。
五、总结
一套 TodoList 把 React 组件通信的四种模式全部串了起来:
- 父传子 :App 通过 props 把
todos传给 TodoList,把total/active/completed传给 TodoStates。数据单向向下流动。 - 子传父 :子组件通过 props 回调函数(
onAdd、onToggle、onDelete、onClearCompleted)通知父组件。子组件不能直接改父状态------这是铁律。 - 本地状态 vs 共享状态 :
inputValue只服务于 TodoInput 的输入体验,放在本地。todos被三个子组件共享,放在 App。学会区分"谁需要这份数据"来决定状态放哪里。 - 计算值 :
activeCount和completedCount不声明 useState,直接从todos派生。每次todos变化自动重算------不引入冗余状态,也不存在同步延迟。 - 不可变更新 :
map+ 展开运算符、filter------ 不修改原对象,返回全新副本。React 靠引用比对判断是否需要重渲染,修改原对象会让你掉进"改了数据但界面不更新"的坑。
组件通信的核心公式:状态上移、props 下传、回调上报。 一个 App 管全部状态,三个子组件各司其职,数据流一清二楚。
------ 组件拆得开,数据流不乱。