《React 状态管理大作战:一个 Todo App 的“家庭伦理剧”》
在前端开发的江湖里,流传着一句老话:
“写个 Todo App,胜过千行 Hello World。”
今天,我们就来围观一场发生在 React 家族内部的“家庭伦理剧”——
父组件(App)如何用爱(状态)和规矩(props)管理三个性格迥异的孩子:TodoInput、TodoList 和 TodoStats。
别担心,这不是枯燥的技术文档,而是一出有笑点、有冲突、有和解的“组件家庭肥皂剧”。准备好了吗?幕布拉开!
🎭 第一幕:家有三宝,各司其职
在这个 React 小家庭里,有三位主角:
-
TodoInput:活泼好动的小儿子,负责“收任务”。
“妈!有人要加新任务啦!”
-
TodoList:认真负责的大女儿,负责“列任务、勾任务、删任务”。
“爸,这条任务已完成,请打钩!”
-
TodoStats:冷静理性的二哥,负责“报数据”。
“全家注意:当前待办 3 条,已完成 2 条,还有 1 条在摸鱼!”
而他们的父母——App 组件——则稳坐中军帐,手握全家唯一的“账本”:todos 数组。
💡 在 React 的世界里,数据只能从父母流向孩子(单向数据流) ,孩子不能擅自改账本,只能“打报告”请求修改。
📜 第二幕:家规第一条——“所有数据归爸妈管!”
const [todos, setTodos] = useState([...]);
这行代码,就是这个家庭的“宪法”。
todos是全家共享的唯一真相源(Single Source of Truth) 。- 三个孩子想看任务?行!爸妈通过
props发下去。 - 想改任务?不行!必须调用爸妈提供的“专用热线”(比如
onAdd,onDelete)。
❌ 错误示范(孩子造反):
// TodoList 内部(错误!)
todo.completed = true; // ❌ 直接改 props?门都没有!
React 会冷冷地说:“Props 是只读的,你没有权限。 ”
✅ 正确操作(打报告):
// TodoList 调用父母给的函数
<button onClick={() => onDelete(todo.id)}>删除</button>
这就像孩子对爸妈说:“我想删掉‘洗碗’这条任务……求批准!”
爸妈审核后,用setTodos更新账本,全家自动同步。
🧩 第三幕:父子通信——“传话筒”与“回调函数”
👨👦 场景 1:小儿子(TodoInput)要加任务
// App 父组件
<TodoInput onAdd={addTodo} />
// TodoInput 子组件
const { onAdd } = props;
// ...
onAdd(inputValue); // 打电话给爸妈:“我要加任务!”
onAdd就是爸妈给小儿子的“专线电话”。- 小儿子一按回车,就拨通电话,把任务内容上报。
- 爸妈收到后,更新
todos,全家重新渲染。
📞 这就是“子 → 父通信”:通过 props 传递函数,子组件调用它来“上报事件”。
👧👦 场景 2:大女儿(TodoList)要删任务或打钩
// App
<TodoList
todos={todos}
onDelete={deleteTodo}
onToggle={toggleTodo}
/>
// TodoList
<input
onChange={() => onToggle(todo.id)}
/>
<button onClick={() => onDelete(todo.id)}>删除</button>
- 大女儿不直接操作
todos,而是调用onToggle或onDelete。 - 爸妈收到指令,用
filter或map生成新数组,替换旧账本。
✅ React 不允许直接修改状态(mutate) ,必须“生成新副本”。
就像不能在原账本上涂改,必须誊抄一本新的!
📊 第四幕:二哥(TodoStats)的“数据播报”
const activeCount = todos.filter(todo => !todo.completed).length;
const completedCount = todos.filter(todo => todo.completed).length;
<TodoStats
total={todos.length}
active={activeCount}
completed={completedCount}
onClearCompleted={clearCompleted}
/>
-
二哥自己不存数据,全靠爸妈实时“喂”。
-
他还会根据数据决定是否显示“清除已完成”按钮:
{completed > 0 && <button>清除已完成任务</button>} -
点击按钮?照样打报告:
onClearCompleted()。
📢 兄弟组件之间从不直接对话!
TodoInput 想知道总任务数?不行!必须问爸妈。
TodoStats 想通知 TodoList 刷新?不行!必须通过爸妈中转。
这就是 “兄弟组件通信 = 父组件中转 + 响应式更新” 。
💾 第五幕:持久化——“断电也不丢任务!”
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
-
爸妈有个“保险柜”(
localStorage),每次账本一变,立刻存档。 -
下次打开页面,先从保险柜取回历史数据:
const [todos, setTodos] = useState(() => { const saved = localStorage.getItem('todos'); return saved ? JSON.parse(saved) : []; });
🔒 数据持久化 = useEffect + localStorage,简单又可靠!
🎨 第六幕:UI 细节——“完成的任务要划掉!”
<li className={todo.completed ? 'completed' : ''}>
-
CSS 配合:
.completed span { text-decoration: line-through; color: #999; } -
勾选框状态由
checked={todo.completed}控制。 -
用户点击?触发
onToggle,爸妈更新状态,UI 自动同步。
✅ React 的核心哲学:UI = f(state)
状态一变,界面自动变,无需手动操作 DOM!
⚠️ 第七幕:常见“家庭矛盾”与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 点添加没反应 | 忘了 e.preventDefault() | 表单提交必须阻止默认刷新! |
| 列表不更新 | 直接修改了数组(如 push) | 用 [...todos, newTodo] 创建新数组 |
| 删除失效 | key 用了 index | 用唯一 id 作 key |
| 数据丢失 | 没做持久化 | 加 useEffect + localStorage |
🏁 终章:React 状态管理的“家庭美德”
这个 Todo App 虽小,却完美体现了 React 的核心思想:
- 单向数据流:数据从父到子,清晰可预测。
- 状态提升:共享状态放在最近的共同父组件。
- 不可变性:永远用新状态替换旧状态,不直接修改。
- 组件职责分离:输入、列表、统计各干各的,互不干扰。
- 响应式更新:状态一变,相关 UI 自动刷新。
💡 真正的工程化,不是炫技,而是让代码像家庭一样——各司其职,沟通有序,和谐运转。
🎁 彩蛋:为什么不用 V-model?
你可能听说 Vue 有 v-model 双向绑定,很方便。
但 React 说:“双向绑定像自由恋爱,容易乱;单向数据流像包办婚姻,虽然老派,但秩序井然! ”
- 单向流让数据流动可追踪、可调试、可预测。
- 虽然多写几行代码,但换来的是大型应用的可维护性。
就像这个家庭:
孩子不能擅自改家规,一切听爸妈安排——
虽然有点“专制”,但家里从不乱套!
下次当你写 React 组件时,不妨想想:
“我的数据,是不是也该有个‘负责任的父母’来统一管理?”
Happy Coding!愿你的状态永不混乱,你的组件永远和睦!😊