🚀 React 实战:手把手带你写一个功能完备的 Todo List
对于 React 初学者来说,Todo List (待办事项清单) 是最经典的入门项目。但写出来和“写得好”是两码事。
今天我们不只是简单的堆砌代码,而是通过一个结构清晰、包含数据持久化(Local Storage)功能的实战案例,来深入理解 组件拆分、状态管理 以及 React Hooks 的最佳实践。
📂 项目结构概览
在开始之前,我们先看看组件是如何拆分的。良好的拆分能让代码更易维护。
App.jsx: 容器组件。负责管理所有数据状态(State)和核心业务逻辑。TodoInput.jsx: 输入组件。负责收集用户输入。TodoList.jsx: 列表组件。负责展示待办项和操作按钮。TodoStats.jsx: 统计组件。负责展示总数、完成数及清理已完成项。
🛠️ 第一步:核心逻辑与状态管理 (App.jsx)
这是整个应用的“大脑”。我们需要处理增删改查,并且利用 localStorage 防止刷新后数据丢失。
亮点解析:
- State 懒初始化:注意
useState里的函数。我们只在组件首次渲染时读取localStorage,避免每次渲染都进行 IO 操作,这是一个常见的性能优化点。 - 副作用处理:利用
useEffect监听todos变化,自动同步到本地存储。
import './stylus/app.styl';
import TodoList from './components/TodoList';
import TodoInput from './components/TodoInput';
import TodoStats from './components/TodoStats';
import { useEffect, useState } from 'react';
function App() {
// 1. 初始化状态:从 LocalStorage 读取,如果无数据则为空数组
// 使用函数式初始化(Lazy Initialization)提升性能
const [todos, setTodos] = useState(() => {
const saved = localStorage.getItem('todos');
return saved ? JSON.parse(saved) : [];
});
// 2. 添加逻辑
const addTodo = (todo) => {
if (!todo.trim()) return; // 简单的非空校验
setTodos([...todos, {
id: Date.now(), // 使用时间戳作为唯一 ID
todo,
completed: false,
}])
}
// 3. 删除逻辑
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id))
}
// 4. 切换完成状态逻辑
const toggleTodo = (id) => {
setTodos(todos.map(todo => todo.id === id ? {
...todo,
completed: !todo.completed,
} : todo))
}
// 5. 清理已完成逻辑
const clearCompleted = () => {
setTodos(todos.filter(todo => !todo.completed))
}
// 6. 衍生状态计算 (Derived State)
// 不需要由于 activeCount 专门定义一个 state,直接计算即可
const activeCount = todos.filter(todo => !todo.completed).length;
const completedCount = todos.filter(todo => todo.completed).length;
// 7. 持久化存储
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos])
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
📝 第二步:受控组件输入 (TodoInput.jsx)
输入框采用 受控组件 (Controlled Component) 模式,即 input 的 value 由 React 的 state 驱动。
import { useState } from "react";
const TodoInput = (props) => {
const { onAdd } = props;
const [inputValue, setInputValue] = useState('');
const handleSubmit = (e) => {
e.preventDefault(); // 阻止表单默认提交行为
onAdd(inputValue); // 调用父组件传递的方法
setInputValue(''); // 清空输入框
}
return (
<div>
<form className="todo-input" onSubmit={handleSubmit}>
<input
type="text"
value={inputValue}
onChange={e => setInputValue(e.target.value)}
placeholder="接下来要做什么?"
/>
<button>Add</button>
</form>
</div>
)
}
export default TodoInput
📋 第三步:列表渲染与交互 (TodoList.jsx)
这里展示了 React 中最常见的列表渲染模式:map。同时,利用三元运算符处理了“空状态”的展示,提升用户体验。
const TodoList = (props) => {
const {
todos,
onDelete,
onToggle
} = props;
return (
<div>
<ul className="todo-list">
{
todos.length === 0 ? (
<li className="empty">No todos yet!</li>
) : (
todos.map(todo => (
<li
key={todo.id} // 必须提供唯一的 Key
className={todo.completed ? 'completed' : ''}
>
<label>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span>{todo.todo}</span>
</label>
<button onClick={() => onDelete(todo.id)}>delete</button>
</li>
))
)
}
</ul>
</div>
)
}
export default TodoList
📊 第四步:数据统计与操作 (TodoStats.jsx)
这是一个典型的纯展示组件(Presentational Component)。它不持有状态,只通过 props 接收数据并展示,或者触发父组件的回调。
小技巧:利用条件渲染
completed > 0 && (...),只有在有已完成项目时才显示“Clear Completed”按钮,界面更简洁。
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
💡 总结与思考
通过这个简单的 Todo List,我们实践了以下 React 核心概念:
- 单向数据流:数据由
App下发,子组件通过调用props中的函数将事件传递回App。 - Hooks 组合拳:
useState管理状态,useEffect处理副作用(持久化)。 - 组件职责分离:逻辑与视图分离,让代码结构一目了然。