React 从零实现TodoList,连小白都能看懂!带你玩转组件、props、key、事件回调!

153 阅读5分钟

前言
你是不是刚入坑 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 是个数组,存放所有待办事项的对象,每个对象有 idtextisComplete
  • 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 旅程一路顺风,代码少报错,项目天天上线!