很多初学 React 的同学绕不明白:子组件为啥不能直接修改父的数据?数组对象更新为什么不能直接改?今天用 TodoList 案例,大白话讲清楚。
先记住 2 条铁律✅ 规则 1:数据统一放在父组件 App 里保管,子组件不能直接动手改这份数据✅ 规则 2:子想要修改数据 → 调用父给的函数,让父自己去改数据
1. 第一块: todos 是什么?
jsx
php
const [todos, setTodos] = useState([
{ id: 1, text: '吃饭', completed: false },
{ id: 2, text: '睡觉', completed: false },
{ id: 3, text: '打豆豆', completed: true }
])
把它理解成:App(爸爸)手里拿着一张清单todos,记录所有待办事项。
todos= 清单本身(只能看,不能直接改)setTodos= 唯一能修改清单的工具
❌ 错误写法:直接 push、直接修改里面对象。React 看不见改动,页面不会刷新。✅ 正确写法:必须使用 setTodos 换一份全新清单。
每条待办的含义:
id:编号,用来区分每一条任务text:任务文字completed:是否完成(true = 完成,false = 未完成)
2. 函数 1:addTodo 添加任务
jsx
vbnet
const addTodo = (text) => {
if (text.trim() === '') return
setTodos([
{id: +Date.now(), text, completed: false },
...todos
])
}
流程
TodoInput(输入框子组件,孩子)用户输入文字,按下回车- 孩子没有权限直接改清单,于是调用爸爸给的
onAdd,把输入文字传给爸爸 - 进入
addTodo
if (text.trim() === '') return:如果输入是空的,直接不处理,禁止空白任务+Date.now():拿当前时间戳当做新任务独一无二的编号[新任务, ...todos]:造一张全新清单,新任务放最上面,旧任务原样带上setTodos(新清单):爸爸替换手里的旧清单 → 页面自动刷新
...todos 大白话:把旧清单里所有任务全部复制出来。
3. 函数 2:toggleTodo 勾选 / 取消任务
jsx
ini
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed}
: todo
))
}
流程
- 用户点击任务前面的复选框(子组件 TodoList)
- 子组件把这条任务的 id 传给爸爸提供的
onToggle map循环旧清单,生成一张全新清单
- 如果当前循环这条任务
id和点击的 id 一致:复制这条任务{...todo},把完成状态取反!todo.completed - 不匹配:直接原样保留这条任务
setTodos替换清单,页面刷新
❌ 禁止直接写 todo.completed = !todo.completed。不能在原对象上修改!
4. 函数 3:deleteTodo 删除任务
jsx
ini
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id))
}
流程
- 用户点击某条任务的删除按钮(子组件 TodoList)
- 子组件把要删除任务的 id 传给爸爸
onDelete filter筛选:只保留 id 不等于要删除 id 的任务,自动剔除目标任务- 返回筛选后的全新清单,
setTodos更新页面
5. 组件调用部分(父子沟通核心)
jsx
ini
<TodoInput onAdd={addTodo}/>
<TodoList
todos={todos}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
<TodoStats todos={todos}/>
形象比喻:
<TodoInput>输入框孩子:爸爸给它一个工具onAdd。孩子输入内容后,喊爸爸:"帮我新增一条任务!"<TodoList>列表孩子:爸爸把清单todos复印件交给它展示。同时给它两个工具:onToggle(切换完成)、onDelete(删除)。孩子点击勾选 / 删除,只能通知爸爸干活,自己不能修改清单原件。<TodoStats>统计孩子:只拿清单复印件,单纯展示已完成多少条,不需要修改,所以不给工具函数。
核心思想一句话总结所有原始数据清单,只保存在父组件 App。子组件只负责展示页面。子想要修改数据,只能调用父提供的函数,由父组件统一修改数据。
完整数据流例子(新增一条任务)
- 用户在输入框打字:
学习React - 回车 → TodoInput 执行
onAdd("学习React") - 父组件
addTodo执行,生成新任务,构建全新任务清单 setTodos更新状态- App 组件刷新,TodoList、TodoStats 拿到新清单,页面自动更新
补充:配套子组件完整代码(可直接运行)
TodoInput.jsx
jsx
javascript
import { useState } from 'react'
export default function TodoInput({onAdd}){
const [value, setValue] = useState('')
const handleKeyDown = (e)=>{
if(e.key === 'Enter'){
onAdd(value)
setValue('')
}
}
return (
<input
value={value}
onChange={(e)=>setValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="请输入待办事项,回车添加"
/>
)
}
TodoList.jsx
jsx
javascript
export default function TodoList({todos,onToggle,onDelete}){
return (
<ul>
{todos.map(item=>(
<li key={item.id}>
<input
type="checkbox"
checked={item.completed}
onChange={()=>onToggle(item.id)}
/>
<span style={{textDecoration:item.completed?'line-through':'none'}}>
{item.text}
</span>
<button onClick={()=>onDelete(item.id)}>删除</button>
</li>
))}
</ul>
)
}
TodoStats.jsx
jsx
javascript
export default function TodoStats({todos}){
const total = todos.length
const finished = todos.filter(i=>i.completed).length
return <div>总共:{total}条,已完成:{finished}条</div>
}
小结
TodoList 看着简单,但是包含 React 最核心的单向数据流、状态提升、不可变数据思想。记住:数据向上集中,交互向下传递回调。所有子组件不要尝试直接修改 props 数据,通知父组件处理状态更新,是标准写法。