前言
你是不是刚入坑 React,看到别人用组件写个 TodoList 感觉秒懂,自己敲起来却一头雾水?
你是不是一直搞不清楚key为啥那么重要?props究竟怎么传递?
还有那个看似简单的onToggle={() => onToggle(todo.id)}到底在干嘛?别急,今天这篇文章,咱们用一个简单的 React TodoList 项目,手把手带你细细讲清楚,从组件拆分到事件传递,从列表渲染到状态更新,保证看完你就是 React 小能手!
注意:本文内容通俗易懂,配合代码和图示,适合小白入门。走起!
一、项目结构梳理:父子组件,数据流到底是咋样?
这项目里一共四个组件:
Todos—— 父组件,负责管理所有待办事项的数据和状态。TodoForm—— 输入表单,负责新增待办事项。TodoList—— 列表容器,负责展示所有待办。TodoItem—— 列表里的每一条待办,展示文字、状态和操作按钮。
为什么这么拆?
React 要求「数据单向流动」:
父组件持有数据,子组件通过 props 接收数据和操作回调,子组件不直接改数据,只负责显示和把用户操作告诉父组件。
这套路让数据管理更清晰,组件更易维护。
二、重点来了:Todos 组件,状态管理的核心
const [todos, setTodos] = useState([
{ id: 1, text: '打豆豆', isComplete: false },
{ id: 2, text: '算法比赛', isComplete: false },
])
todos是个数组,存放所有待办事项的对象,每个对象有id、text和isComplete。setTodos是用来更新todos的函数,React 看到它变化,自动帮你刷新界面!
1. 新增待办:addTodo
const addTodo = (text) => {
setTodos([
...todos,
{ id: Date.now(), text, isComplete: false }
])
}
- 拿当前
todos用扩展运算符...todos复制一份。 - 加入一个新对象(生成唯一
id,文本和默认状态)。 - 重新调用
setTodos,触发 React 渲染。
2. 切换完成状态:onToggle
const onToggle = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, isComplete: !todo.isComplete } : todo
))
}
- 遍历
todos,找到对应id的待办,把isComplete取反(完成变未完成,未完成变完成)。 - 返回一个全新的数组(千万别直接修改原数组,React 依赖状态不可变)。
- 通过
setTodos更新。
3. 删除待办:onDelete
const onDelete = (id) => {
setTodos(todos.filter(todo => todo.id !== id))
}
- filter()过滤掉
id对应的待办,得到新数组。 - 更新状态。
三、TodoForm:小小表单的大用处
const [text, setText] = useState('')
- 这是本组件的私有状态,负责实时保存用户输入的内容。
<form onSubmit={handleSubmit}>
<input
value={text}
onChange={e => setText(e.target.value)}
placeholder='Todo text'
required
/>
<button type='submit'>Add</button>
</form>
onChange事件绑定了更新状态,保证输入框内容和text保持同步(受控组件)。handleSubmit函数防止默认刷新,去除空格,调用父组件传过来的onAddTodo,把新任务发回父组件处理,最后清空输入框。
四、TodoList:干啥的?渲染列表的管家
todos.length > 0 ? (
todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={() => onToggle(todo.id)}
onDelete={() => onDelete(todo.id)}
/>
))
) : (
<p>暂无待办事项</p>
)
重点讲讲这段
- 为什么要写
key={todo.id}?
React 需要key来唯一标识列表中每个元素,方便它用高效的 Diff 算法找出变化,避免不必要的重新渲染。
没写会警告,写错会导致输入框跳动、状态错乱。 - 为什么传
todo={todo}?
把整个待办对象传给子组件,让TodoItem自己用数据渲染内容。 onToggle={() => onToggle(todo.id)}是啥?
这其实是给子组件传了一个「包裹了当前 todo.id 的函数」,子组件调用这个函数时,父组件的onToggle就知道是哪个待办要切换状态。
五、TodoItem:待办事项的具体表现
const { id, text, isComplete } = props.todo
return (
<div className="todo-item">
<input type="checkbox" checked={isComplete} onChange={onToggle} />
<span className={isComplete ? 'complete' : ''}>{text}</span>
<button onClick={onDelete}>Delete</button>
</div>
)
checked={isComplete}根据状态勾选复选框。className={isComplete ? 'complete' : ''},动态添加类名,配合 CSS 实现划线和变灰效果,帮你一眼看出完成状态。- 按钮点击删除,复选框变化触发切换状态,都是调用父组件传来的函数,改变父组件的数据状态。
六、全程贯穿的 React 核心思想
- 单向数据流:父组件持有状态,数据通过 props 传给子组件。
- 状态不可变:操作数组/对象时用新副本,不能直接修改。
- 受控组件:表单输入框的值由 state 控制,保证数据和视图一致。
- 列表渲染必须有
key,优化性能且避免渲染bug。 - 事件回调传参数的套路:
onToggle={() => onToggle(todo.id)}用箭头函数包一层,防止立即执行,还能携带参数。
七、代码小彩蛋和踩坑提示
-
handleSubmit里写错了:e.prventDefault()应该是e.preventDefault(),小心拼写错误! -
如果用索引当
key,增删列表时会出大事,状态错乱,别用!如果面试被问到怎么回答?这样回答包满分!:在React中,key用来唯一标识每个元素,帮助react高效的进行虚拟DOM的diff算法,如果用数组索引,因为数组索引会因为增删而发生改变,导致react认为元素身份也发生了改变,从而造成不必要的DOM重建和状态错乱。
-
React 的
key只给 React 自己用,子组件里拿不到props.key,别去访问。 -
组件首字母必须大写,否则 React 不会当成组件解析。
-
className是 React JSX 里写 class 的方式,别写错成class。
八、总结
这篇文章讲透了一个 TodoList React 项目从结构到细节的全套知识点:
- 组件拆分
- 状态管理
- 受控组件
- 列表渲染
- 事件传递
key的秘密
希望你看完不仅能写能改,还能面试顺利通关,做个快乐的 React 小码农!
如果你觉得好用,点个收藏,转给身边学 React 的小伙伴吧!
有任何问题或者想要更深入的讲解,留言告诉我,咱们下篇再见!
祝你 React 旅程一路顺风,代码少报错,项目天天上线!