news 2026/7/25 19:12:41

从零到一理解 React 父子组件通信:手写一个 Todo 应用带你彻底搞懂单向数据流

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零到一理解 React 父子组件通信:手写一个 Todo 应用带你彻底搞懂单向数据流

拒绝背八股,从一个真实场景出发,吃透 React 组件间通信的底层逻辑


写在前面

很多初学者在学习 React 时,最先被“卡住”的地方往往不是 JSX 语法,也不是 hooks 的用法,而是一个看似简单却至关重要的问题:组件之间到底怎么通信?

尤其是父子组件,props 传参、回调函数、状态提升……这些概念单独拎出来都懂,一写代码就懵。

今天,我们不聊虚的。我带你从零到一实现一个完整的 Todo 应用,边写边讲,把 React 父子组件通信的每一个细节都掰开揉碎。你会发现,当你真正理解了单向数据流,这些问题都会迎刃而解。

一句话总结本文核心:数据在父组件中持有,通过 props 向下传递;变化通过回调函数向上通知,父组件修改数据后,子组件自动更新。


一、从组件化思维开始

在 React 的世界里,一切皆为组件。组件化不仅仅是一种代码组织方式,更是一种思维方式

1.1 拿到设计稿,先画组件树

无论是做一个 Todo 应用,还是搭建一个企业级后台,第一步都不是写代码,而是“拆”

把 UI 拆解成独立的、可复用的组件,并规划好它们之间的层级关系。这就是所谓的组件树(Component Tree)

对于我们的 Todo 应用,界面可以拆分成三块:

App (根组件) ├── TodoInput (输入框 + 添加按钮) ├── TodoList (列表) │ └── TodoItem (列表项 - 在 TodoList 内部渲染) └── TodoState (统计 + 清除按钮)

对应的目录结构是这样的:

src/ ├── App.jsx ├── App.css ├── components/ │ ├── TodoInput.jsx │ ├── TodoList.jsx │ └── TodoState.jsx └── main.jsx

为什么先做这一步?因为在 vibe coding 时代,清晰的组件结构能让你的思路始终保持在线。有了这张“地图”,代码往哪里写、数据往哪里放,一目了然。


二、核心原则:单向数据流

在 React 中,有一条铁律:数据流向是单向的,从父组件到子组件

说白了就是:父组件是“老板”,子组件是“员工”。老板掌握核心数据,员工只负责干活,通过老板给的“指令”(props)来工作。员工不能擅自改数据,只能向老板“打报告”(调用回调函数),由老板来决策是否修改。

这张图,刻在脑子里:

数据 (state) → 通过 props 传递 → 子组件渲染 ↑ │ │ │ └────── 回调函数 (callback) ←─────────┘ 子组件通过调用回调 通知父组件更新数据

三、实战演练:Todo 应用完整实现

下面我们一步步实现这个 Todo 应用,每一行代码都会对应到上面的原则。

3.1 父组件 App:数据的“大本营”

父组件 App 是整个应用的数据持有者。所有的 Todo 数据都存放在这里,所有修改数据的方法也在这里定义。

// App.jsx import { useState } from 'react'; import TodoInput from './components/TodoInput.jsx'; import TodoList from './components/TodoList.jsx'; import TodoState from './components/TodoState.jsx'; import './App.css'; const App = () => { // ✅ 数据状态由父组件持有和管理 const [todos, setTodos] = useState([ { id: 1, text: '吃饭', completed: false }, { id: 2, text: '睡觉', completed: false }, { id: 3, text: '打豆豆', completed: true }, ]); // ✅ 添加 todo —— 父组件负责修改 const addTodo = (text) => { if (text.trim() === '') return; // ⚠️ 注意:不能直接 push 修改原数组! // todos.push({ id: Date.now(), text }); // ❌ 错误做法 // 必须创建一个全新的状态 setTodos([{ id: +Date.now(), text, completed: false }, ...todos]); }; // ✅ 切换完成状态 const toggleTodo = (id) => { setTodos(todos.map(todo => todo.id === id ? { ...todo, completed: !todo.completed } : todo )); }; // ✅ 删除 todo const deleteTodo = (id) => { setTodos(todos.filter(todo => todo.id !== id)); }; // ✅ 清除已完成的 const clearCompleted = () => { setTodos(todos.filter(todo => !todo.completed)); }; // 派生数据:从现有数据计算得出 const activeCount = todos.filter(t => !t.completed).length; const completedCount = todos.filter(t => t.completed).length; return ( <div> <h1>My Todo List</h1> {/* 通过 props 向下传递数据和方法 */} <TodoInput onAdd={addTodo} /> <TodoList todos={todos} onToggle={toggleTodo} onDelete={deleteTodo} /> <TodoState total={todos.length} active={activeCount} completed={completedCount} onClearCompleted={clearCompleted} /> </div> ); }; export default App;

划重点:

  • todos数组是唯一的数据源,由useState管理
  • 所有修改操作(增、删、改、清)都通过setTodos生成新数组来更新
  • 永远不要直接修改 state,React 依赖不可变数据来触发重新渲染

为什么不能直接修改 state?因为 React 做的是浅比较。直接todos.push()修改的是同一个数组引用,React 认为“没变化”,自然不会重新渲染。而setTodos([...])创建了新数组,引用变了,React 才会知道“哦,数据变了,我得重新渲染”。

3.2 子组件 TodoInput:表单交互

TodoInput 负责用户输入和提交。它自己需要维护一个输入框的inputValue状态——但注意,这个状态是它自己的私有状态,不需要共享给父组件

// components/TodoInput.jsx import { useState } from 'react'; const TodoInput = ({ onAdd }) => { // 这是子组件自己的状态,与父组件无关 const [inputValue, setInputValue] = useState(''); const handleSubmit = (e) => { e.preventDefault(); // ✅ 当需要报告父组件时,调用父组件传过来的回调函数 onAdd(inputValue); setInputValue(''); // 清空输入框 }; return ( <form className="todo-input" 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> ); }; export default TodoInput;

通信模式拆解:

方向方式数据内容
父→子propsonAdd回调函数
子→父调用onAdd输入框中的文本inputValue

TodoInput 自己管理inputValue,但数据真正的归宿是父组件。用户点击提交时,子组件“打报告”给父组件,父组件执行addTodo将数据加入列表。这就是所谓的“子组件只负责收集,不负责存储”

3.3 子组件 TodoList:列表渲染

TodoList 接收父组件传来的todos数组,负责渲染列表。同时,它把onToggleonDelete回调继续向下传递(给 TodoItem)。

这里我直接把 TodoItem 内联在 TodoList 中渲染,方便展示:

// components/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)}>Delete</button> </li> )) )} </ul> ); }; export default TodoList;

通信模式拆解:

方向方式数据内容
父→子propstodos,onToggle,onDelete
子→父调用onToggle(id)/onDelete(id)目标 todo 的id

TodoList 是一个典型的展示型组件:它只负责“怎么显示”,不关心“数据怎么来”“改了会怎样”。所有的逻辑都委托给父组件处理。

3.4 子组件 TodoState:状态统计

// components/TodoState.jsx const TodoState = ({ total, active, completed, onClearCompleted }) => { return ( <div className="todo-state"> <p>Total: {total} | Active: {active} | Completed: {completed}</p> {completed > 0 && ( <button onClick={onClearCompleted} className="clear-btn"> Clear Completed </button> )} </div> ); }; export default TodoState;

通信模式拆解:

方向方式数据内容
父→子propstotal,active,completed,onClearCompleted
子→父调用onClearCompleted()无参数,仅表示“清除已完成”这个动作

四、深入理解:为什么子组件不能直接修改父组件的 state?

这是 React 新手最容易踩的坑之一。

从设计层面看:如果子组件能随意修改父组件的状态,数据流就会变得混乱。当 Bug 出现时,你根本不知道是哪个子组件偷偷改了数据。调试难度呈指数级上升。

从 React 机制看:父组件的 state 是“私有财产”,子组件根本没有访问它的权限。子组件能拿到的只有 props,而 props 是只读的

记住这句话:数据只能向下流,变化只能向上报。

正确的做法就是我们的代码所示:子组件通过调用props中的回调函数,“请求”父组件修改数据。父组件收到请求后,自主决定是否修改、如何修改。


五、补充:useState 的懒执行(Lazy Initialization)

顺带提一个点:useState懒执行

当初始值的计算开销较大时(比如涉及复杂运算、读取 localStorage 等),可以使用函数形式的 initialState:

// ❌ 每次渲染都会执行 getInitialValue,即使只在首次使用 const [state, setState] = useState(getInitialValue()); // ✅ 只有在首次渲染时才会执行 getInitialValue const [state, setState] = useState(() => getInitialValue());

在 Todo 应用中,如果你的初始数据需要从 localStorage 读取,可以这样写:

const [todos, setTodos] = useState(() => { const saved = localStorage.getItem('todos'); return saved ? JSON.parse(saved) : []; });

这个细节很小,但在性能敏感的场景下,能帮你避免不必要的计算。


六、踩坑记录 & 最佳实践

坑 1:直接修改 state

// ❌ 错误 const addTodo = (text) => { todos.push({ id: Date.now(), text }); // 直接修改原数组 setTodos(todos); };

后果:React 检测不到变化,界面不更新。

正确做法:永远使用setTodos([...])创建新数组。

坑 2:在子组件中“缓存” props

// ❌ 错误 const TodoItem = ({ todo }) => { const [completed, setCompleted] = useState(todo.completed); // 危险! // ... };

后果:当父组件的todo.completed变化时,子组件的completed不会同步更新,因为useState只在首次渲染时生效。

正确做法:直接在 JSX 中使用props.todo.completed,不要用 state 去“复制” props。

坑 3:回调函数中忘记传递参数

// ❌ 错误 <button onClick={onDelete}>Delete</button> // 没有传 id // ✅ 正确 <button onClick={() => onDelete(todo.id)}>Delete</button>

最佳实践清单

  • 状态提升:多个子组件共享的数据,提升到最近的共同父组件中管理

  • 不可变数据:使用扩展运算符[...]{...}mapfilter等创建新数据

  • props 只读:子组件绝不修改 props 中的数据

  • 回调命名规范:用on开头,如onAddonToggleonDelete,一看就知道是事件

  • 单一数据源:一个数据只在一个地方管理,不要多处存副本


七、总结

回顾一下,我们从零实现了一个完整的 Todo 应用,核心只有三点:

  1. 状态在哪里?在父组件 App 中,通过useState持有todos数据。
  2. 数据怎么传递?通过props从父组件流向子组件。
  3. 数据怎么修改?子组件通过调用父组件传递的回调函数,向父组件发送“修改请求”。

这就是 React 最经典的父子组件通信模式,也是单向数据流的最佳实践。

最后送你一句话:在 React 的世界里,数据像水一样往下流,变化像信号一样往上发。理解了这一点,你就能驾驭任何复杂的组件通信。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/25 19:12:01

深度强化学习入门:从PPO、DQN到A3C的算法对比与实践指南

这次我们来看一个面向新手的深度强化学习入门教程。这个教程的核心目标不是让你立刻成为算法专家&#xff0c;而是帮你快速理解强化学习到底在解决什么问题&#xff0c;以及那些听起来高大上的算法&#xff08;比如PPO、DQN、A3C&#xff09;在实际中是如何运作的。对于想入门A…

作者头像 李华
网站建设 2026/7/25 19:11:50

LTX2.3+ComfyUI:AI视频生成环境搭建与漫剧制作实战指南

最近在AI视频生成领域&#xff0c;LTX2.3的发布确实引起了不小的轰动。作为Lightricks开源音视频生成模型的最新版本&#xff0c;它在细节表现、人像视频质量和音频处理方面都有了显著提升。特别是结合ComfyUI的可视化工作流&#xff0c;让普通用户也能轻松上手制作高质量的AI视…

作者头像 李华
网站建设 2026/7/25 19:11:31

KMS智能激活脚本:三步实现Windows和Office永久激活的完整指南

KMS智能激活脚本&#xff1a;三步实现Windows和Office永久激活的完整指南 【免费下载链接】KMS_VL_ALL_AIO Smart Activation Script 项目地址: https://gitcode.com/gh_mirrors/km/KMS_VL_ALL_AIO 还在为Windows系统激活和Office软件授权而烦恼吗&#xff1f;KMS_VL_AL…

作者头像 李华
网站建设 2026/7/25 19:08:36

AssetStudio从入门到精通:Unity游戏资源提取与逆向工程实战指南

1. 项目概述&#xff1a;为什么我们需要AssetStudio&#xff1f;如果你曾经对一款Unity游戏里的精美模型、酷炫特效或者独特的UI界面产生过好奇&#xff0c;想知道它们是怎么做出来的&#xff0c;甚至想自己拿来研究或进行二次创作&#xff0c;那么你很可能需要AssetStudio。这…

作者头像 李华
网站建设 2026/7/25 19:07:20

Unity与Visual Studio智能提示失效的深度诊断与修复指南

1. 问题根源与诊断&#xff1a;为什么Unity和VS会“失联”&#xff1f;如果你是一名Unity开发者&#xff0c;十有八九遇到过这个令人抓狂的场景&#xff1a;在Visual Studio里打开C#脚本&#xff0c;满怀期待地敲下几个字母&#xff0c;却发现那个本该如影随形的智能提示框&…

作者头像 李华