很多前端新手在初学 React 时,往往觉得 TodoList 只是一个简单的"增删改查"玩具。但事实上,它是检验你是否真正掌握 React 核心设计哲学的最佳试金石。
为什么这么说?因为一个优秀的 TodoList,完美浓缩了 React 的三大基石:组件化思想、单向数据流、以及 Hooks 状态管理。今天,我们就通过拆解一个经典的 TodoList 源码,带你从底层逻辑出发,彻底打通 React 开发的任督二脉!
🧱 一、 组件化思维:像搭乐高一样规划项目
在动手写代码之前,最重要的事情是"规划组件树"。优秀的代码结构不仅能降低维护成本,还能极大提升 AI 辅助编程(Vibe Coding)的准确率。
在这个实战中,我们将 UI 拆分成了四个职责单一的组件:
- App(根组件) :整个应用的数据中心,负责"持有数据"和"管理逻辑"。
- TodoInput(输入组件) :专注处理用户输入与表单提交。
- TodoList(列表组件) :专注展示任务列表,处理完成/删除的 UI 交互。
- TodoStats(统计组件) :专注展示剩余/已完成数量,并提供清除功能。
💡 深度解析:
这种拆分体现了 React 的单一职责原则 。每个组件只关心自己的 UI 渲染和局部逻辑。当我们需要修改"清空已完成任务"的功能时,只需要去看 TodoStats 和 App,而完全不用碰 TodoInput,这就是组件化带来的高内聚、低耦合。
🔄 二、 单向数据流:子组件如何"合法"修改父组件状态?
这是初学者最容易踩坑的地方。React 的铁律是:数据是单向流动的,子组件绝对不能直接修改父组件的状态(Props 是只读的) 。
那么,当用户在 TodoInput 中输入任务并点击添加时,数据是如何更新到 App 组件中的呢?答案是:状态提升(Lifting State Up)+ 自定义事件回调。
1. 父组件下发"修改方法"
在 App 组件中,我们定义了修改数据的函数,并通过 Props 传递给子组件:
ini
// App.jsx
<TodoInput onAdd={addTodo} />
<TodoList todos={todos} onToggle={toggleTodo} onDelete={deleteTodo} />
2. 子组件触发"自定义事件"
在 TodoInput 中,当表单提交时,我们调用父组件传下来的 onAdd 方法,并将输入的值作为参数传回去:
scss
// TodoInput.jsx
const handleSubmit = (e) => {
e.preventDefault(); // 💡 细节:阻止表单默认的刷新页面行为
onAdd(inputValue); // 💡 核心:通知父组件去修改状态
setInputValue(''); // 💡 细节:提交后清空输入框,提升用户体验
}
💡 深度解析:
这种模式被称为受控组件(Controlled Components) 。输入框的值由 React 的 useState 控制,用户的输入触发 onChange,进而更新 State 并重新渲染视图。这保证了数据和 UI 的绝对同步,避免了传统 DOM 操作中数据与视图不一致的痛点。
🛠️ 三、 状态更新的铁律:永远不要直接修改原数组
在 React 中,状态(State)必须是不可变数据(Immutable Data) 。这意味着我们不能用 push、splice 等方法直接修改原数组,而是必须创建一个全新的数组引用,React 才能检测到变化并触发重新渲染。
在这个 TodoList 中,我们展示了最经典的数组状态更新三板斧:
1. 添加任务(展开运算符)
javascript
const addTodo = (text) => {
if(text.trim() === '') return; // 💡 细节:防御性编程,防止添加空任务
setTodos([
{ id: Date.now(), text, completed: false }, // 💡 细节:使用当前时间戳作为唯一标识
...todos, // 将旧数据展开,新任务放在最前面
]);
}
2. 切换状态(Map 遍历映射)
ini
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed } // 💡 核心:找到目标,用展开运算符创建一个新对象并翻转状态
: todo // 其他项保持原样
));
}
3. 删除任务(Filter 过滤)
ini
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id)); // 💡 核心:返回一个不包含目标 ID 的新数组
}
💡 深度解析:
这三步操作(展开、Map、Filter)都没有改变原来的 todos 数组,而是每次都返回了全新的数组。这不仅符合 React 的更新机制,还能在未来配合 React.memo 或 useMemo 等性能优化手段,避免不必要的组件重渲染。
📊 四、 条件渲染与状态驱动 UI
React 是声明式的,这意味着 UI 是状态的直接映射。在 TodoList 和 TodoStats 中,我们完美展示了状态是如何驱动界面的:
ini
// 列表为空时的优雅降级
{todos.length === 0 ? (
<li className="empty">No todos yet!</li>
) : (
todos.map(todo => ( /* 渲染列表 */ ))
)}
// 只有当有已完成任务时,才显示"清除"按钮
{completed > 0 && (
<button onClick={onClearCompleted}>Clear Completed</button>
)}
💡 深度解析:
利用三元表达式和 && 逻辑短路,我们可以极其优雅地处理各种 UI 状态。不需要手动去操作 DOM 的 display: none,只要状态变了,React 会自动帮你把界面更新到正确的样子。
📝 总结:从 TodoList 看 React 开发内功
回顾整个开发流程,我们可以提炼出以下核心内功:
- 先规划再动手:理清组件树结构,明确每个组件的职责。
- 数据驱动一切:摒弃直接操作 DOM 的思维,让 State 成为唯一的真理来源(Single Source of Truth)。
- 掌握不可变更新:熟练使用展开运算符、Map 和 Filter 来处理复杂状态。
- 熟练运用通信机制:父传子用 Props,子传父用回调函数。
掌握了这些,你就不再是 React 的门外汉,而是真正理解了现代前端框架的设计哲学!