React 组件通信:一套 TodoList 讲清 props 与回调

摘要:React 组件之间怎么传数据?父传子靠 props,子传父靠回调。本文用一套完整的 TodoList 项目------App 管状态、TodoInput 上报新增、TodoList 展示与操作、TodoStates 底部统计------把四种通信模式串成一条完整的组件树,外加三个增删改操作的数据流拆解。


目录

  1. 组件树:先规划结构,再动手写代码
  2. [父传子: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")
  3. 子传父:回调函数让事件向上冒
  4. 三个核心操作的数据流拆解
  5. 总结

一、组件树:先规划结构,再动手写代码

在写第一行 JSX 之前,先画出组件树。一套 TodoList 应用可以拆成四个组件:

flowchart TD A[App<br/>状态持有者<br/>todos 数组] --> B[TodoInput<br/>输入框+添加按钮<br/>通过回调上报父组件] A --> C[TodoList<br/>列表渲染<br/>通过props接收数据] A --> D[TodoStates<br/>底部统计栏<br/>通过props接收统计]
组件 职责 持有的状态 从父组件接收
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>
);
flowchart LR A[App<br/>todos 数组<br/>activeCount<br/>completedCount] -->|todos 数组| B[TodoList<br/>接收数组 渲染列表] A -->|统计数字| C[TodoStates<br/>接收统计 展示计数] A -->|onAdd 回调| D[TodoInput<br/>接收回调 上报新增]

注意 activeCountcompletedCount 不是 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

flowchart LR A[TodoInput<br/>用户输入回车] --> B[handleSubmit<br/>调 onAdd text] B --> C[App.addTodo<br/>setTodos] C --> D[新todos数组<br/>新对象加旧数组] D --> E[React 重渲染] E --> F[TodoList 收到新props<br/>列表自动刷新]
jsx 复制代码
const addTodo = (text) => {
    setTodos([
        { id: Date.now(), text, completed: false },
        ...todos
    ]);
};

Date.now() 做临时 id(生产环境应使用 crypto.randomUUID()),新对象放在数组开头------新加的 todo 显示在最上面。

切换完成状态

flowchart LR A[TodoList<br/>用户点击checkbox] --> B[onChange<br/>调 onToggle id] B --> C[App.toggleTodo<br/>setTodos] C --> D[todos.map<br/>匹配id翻转completed] D --> E[React 重渲染] E --> F[该行className切换<br/>checkbox勾选状态更新]
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 组件通信的四种模式全部串了起来:

  1. 父传子 :App 通过 props 把 todos 传给 TodoList,把 total/active/completed 传给 TodoStates。数据单向向下流动。
  2. 子传父 :子组件通过 props 回调函数(onAddonToggleonDeleteonClearCompleted)通知父组件。子组件不能直接改父状态------这是铁律。
  3. 本地状态 vs 共享状态inputValue 只服务于 TodoInput 的输入体验,放在本地。todos 被三个子组件共享,放在 App。学会区分"谁需要这份数据"来决定状态放哪里。
  4. 计算值activeCountcompletedCount 不声明 useState,直接从 todos 派生。每次 todos 变化自动重算------不引入冗余状态,也不存在同步延迟。
  5. 不可变更新map + 展开运算符、filter ------ 不修改原对象,返回全新副本。React 靠引用比对判断是否需要重渲染,修改原对象会让你掉进"改了数据但界面不更新"的坑。

组件通信的核心公式:状态上移、props 下传、回调上报。 一个 App 管全部状态,三个子组件各司其职,数据流一清二楚。


------ 组件拆得开,数据流不乱。

相关推荐
渣波1 小时前
别把 TodoList 写成面条代码!一文吃透 React 组件通信与状态驱动的核心内功
前端
2601_964702891 小时前
Claude Opus 5 API 开发实战:对话、文本生成与结构化输出
java·服务器·前端
cellurw1 小时前
20260724 六组件编译收尾与全局构建验证
前端
用户2930750976691 小时前
用React 完成Todos 的 组件应用:深入理解单向数据流与组件通信
前端
玉宇夕落2 小时前
React 父子组件通信 Todo List 看透单向数据流与不可变数据
前端
软件开发技术深度爱好者2 小时前
国际音标魔法实验室工具HTML5实现
前端·html5·英语学习
FogLetter2 小时前
嘘!WebSocket正在“偷听”你的网络请求——全双工通信的魔法
前端·面试
用户938515635072 小时前
从零构建 React Todo 应用:组件化设计与数据流深度解析
前端·javascript