React组件通信:从零搭建TodoList的积木式思维
"组件通信不是魔法,而是积木的拼接艺术。" —— 一个被React代码淹没的前端工程师
一、为什么我们需要组件通信?
想象一下,你正在搭建一个乐高城堡。城堡的每个部分(墙、塔、门)都是独立的模块,但它们必须通过某种方式连接在一起,才能构成一个完整的城堡。在React中,组件就是这些"乐高模块",而组件通信就是连接它们的"乐高连接器"。
今天,我们将通过一个简单的TodoList应用,深入理解React组件通信的核心原理。让我们一起用积木思维搭建这个应用,看看如何实现"父组件持有数据,子组件请求修改"的模式。
核心概念:数据流的"单行道"
在React中,数据流动是单向的:
- 父组件持有数据状态(
useState) - 父组件通过
props将数据和修改数据的方法传递给子组件 - 子组件不能直接修改父组件的数据,只能通过回调函数通知父组件
- 父组件收到通知后,用新状态替换旧状态(使用
...展开运算符或filter等方法)
为什么不能直接修改?因为React的虚拟DOM需要知道"哪里变化了",直接修改原始数据会导致React无法追踪变化,破坏应用的响应性。
二、App.jsx:积木的"城堡地基"
import { useState, useEffect } from 'react';
import './styles/app.styl';
import TodoList from './components/TodoList';
import TodoInput from './components/TodoInput';
import TodoStats from './components/TodoStats';
function App() {
// 父组件持有数据状态
const [todos, setTodos] = useState([]);
// 添加任务:使用...展开创建新数组
const addTodo = (text) => {
setTodos([...todos, { id: Date.now(), text, completed: false }]);
};
// 删除任务:使用filter筛选出不匹配ID的任务
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
// 切换任务状态:使用map遍历并更新特定任务
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
// 清除已完成:使用filter筛选出未完成的任务
const clearCompleted = () => {
setTodos(todos.filter(todo => !todo.completed));
};
// 计算统计信息
const activeCount = todos.filter(todo => !todo.completed).length;
const completedCount = todos.filter(todo => todo.completed).length;
return (
<div className="todo-app">
<h1>My Todo List</h1>
<TodoInput onAdd={addTodo} />
<TodoList
todos={todos}
onDelete={deleteTodo}
onToggle={toggleTodo}
/>
<TodoStats
total={todos.length}
active={activeCount}
completed={completedCount}
onClearCompleted={clearCompleted}
/>
</div>
);
}
export default App;
关键点解析:
- 状态持有者:App组件是整个应用的"地基",它使用
useState管理todos状态。 - 方法传递:将修改状态的方法(如
addTodo,deleteTodo等)作为props传递给子组件。 - 计算属性:
activeCount和completedCount是基于状态计算得出的派生数据。
为什么这样设计?
这是React的核心理念:单一数据源。所有状态都由父组件持有,子组件只能请求修改,不能直接操作。这确保了状态的可预测性和可维护性。
三、TodoInput.jsx:添加待办事项的"输入口"
import { useState } from 'react'
const TodoInput = (props) => {
const { onAdd } = props;
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)}
/>
<button type="submit">Add</button>
</form>
)
}
export default TodoInput
关键点解析:
- 单向数据流:React不支持双向绑定(如Vue的v-model),而是通过
value和onChange实现单向数据流。 - preventDefault:
e.preventDefault()阻止了表单的默认提交行为(页面刷新),这是处理表单的关键。 - 子组件请求:当用户提交表单时,子组件调用父组件传递的
onAdd方法,而不是直接修改状态。
为什么用preventDefault?
浏览器默认会提交表单并刷新页面,preventDefault阻止了这一行为,让React可以处理表单数据,避免页面刷新。
四、TodoList.jsx:待办事项的"展示与操作区"
const TodoList = (props) => {
const { todos, onDelete, onToggle } = props;
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)}>X</button>
</li>
))
)}
</ul>
)
}
export default TodoList
关键点解析:
- 列表渲染:使用
map遍历todos数组,为每个待办事项生成列表项。 - 条件渲染:
todos.length === 0时显示空列表提示。 - 状态展示:
className={todo.completed ? 'completed' : ''}根据完成状态应用CSS类。 - 事件处理:
onChange和onClick触发子组件请求,调用父组件传递的onToggle和onDelete方法。
为什么用filter?
filter方法创建新数组,保留满足条件的元素。例如,todos.filter(todo => !todo.completed)会创建一个不包含已完成事项的新数组,用于计算活跃项数量。
五、TodoStats.jsx:统计信息的"仪表盘"
const TodoStats = (props) => {
const { total, active, completed, onClearCompleted } = props
return (
<div className="todo-stats">
<p>Total: {total} | Active: {active} | Completed: {completed}</p>
{completed > 0 && (
<button onClick={onClearCompleted} className="clear-btn">
Clear Completed
</button>
)}
</div>
)
}
export default TodoStats
关键点解析:
- 条件渲染:
{completed > 0 && (...)}确保只有当有已完成事项时才显示"Clear Completed"按钮。 - 按钮功能:点击按钮触发
onClearCompleted方法,清空已完成的待办事项。
六、组件通信的"积木式"思维
让我们用"积木"的比喻来理解组件通信:
-
父组件是"城堡地基" :持有所有数据和修改数据的方法。
-
子组件是"乐高积木" :每个积木只能通过"连接器"(props)与地基通信。
-
通信方式:
- 父→子:通过props传递数据和方法
- 子→父:子组件调用父组件传递的方法,请求修改数据
为什么不能子组件直接修改父组件状态?
这是React的核心设计原则:单向数据流。如果子组件能直接修改父组件状态,会导致状态管理混乱,难以追踪数据变化。通过"请求-响应"模式,我们可以清晰地知道状态变化的来源。
七、总结:组件通信的"积木哲学"
- 单一数据源:所有状态由父组件持有
- 单向数据流:父→子通过props传递数据和方法
- 请求-响应模式:子组件请求修改,父组件响应修改
- 不可变性:通过创建新状态而非修改原状态,确保状态可预测
在TodoList应用中,我们看到了这种模式的完美体现:
- App(父)持有todos状态
- TodoInput(子)请求添加新待办事项
- TodoList(子)请求删除或标记完成
- TodoStats(子)请求清除已完成事项
组件通信不是复杂的魔法,而是清晰的积木拼接。 通过"父组件持有数据,子组件请求修改"的模式,我们构建了一个可维护、可预测、易于调试的应用。