Appearance
第14章:基础实战:TodoList(新手入门必做)
TodoList 是 React 新手的经典实战项目,通过实现一个完整的 TodoList 应用,我们可以巩固之前学习的 React 基础知识,包括组件拆分、状态管理、事件处理等。本章将详细介绍如何构建一个功能完整的 TodoList 应用。
14.1 需求分析与页面结构设计
14.1.1 功能需求
- 添加新任务
- 标记任务为已完成/未完成
- 编辑任务内容
- 删除任务
- 显示任务总数和已完成任务数
- 清除已完成任务
- 本地存储持久化
14.1.2 页面结构设计
TodoList
├── TodoHeader
│ ├── 输入框
│ └── 添加按钮
├── TodoList
│ └── TodoItem (多个)
│ ├── 复选框
│ ├── 任务内容
│ ├── 编辑输入框
│ └── 删除按钮
└── TodoFooter
├── 任务统计
└── 清除已完成按钮14.2 组件拆分(TodoHeader、TodoList、TodoItem、TodoFooter)
首先,我们需要创建项目结构并拆分组件。
14.2.1 项目结构
src/
├── components/
│ ├── TodoHeader.js
│ ├── TodoList.js
│ ├── TodoItem.js
│ └── TodoFooter.js
├── App.js
└── index.js14.2.2 创建基础组件
TodoHeader 组件
jsx
// src/components/TodoHeader.js
import React, { useState } from 'react';
function TodoHeader({ onAddTodo }) {
const [inputValue, setInputValue] = useState('');
const handleInputChange = (e) => {
setInputValue(e.target.value);
};
const handleSubmit = (e) => {
e.preventDefault();
if (inputValue.trim()) {
onAddTodo(inputValue.trim());
setInputValue('');
}
};
return (
<header className="todo-header">
<h1>Todo List</h1>
<form onSubmit={handleSubmit}>
<input
type="text"
value={inputValue}
onChange={handleInputChange}
placeholder="请输入任务内容"
className="todo-input"
/>
<button type="submit" className="add-button">
添加
</button>
</form>
</header>
);
}
export default TodoHeader;TodoItem 组件
jsx
// src/components/TodoItem.js
import React, { useState } from 'react';
function TodoItem({ todo, onToggle, onUpdate, onDelete }) {
const [isEditing, setIsEditing] = useState(false);
const [editValue, setEditValue] = useState(todo.text);
const handleToggle = () => {
onToggle(todo.id);
};
const handleEdit = () => {
setIsEditing(true);
};
const handleEditChange = (e) => {
setEditValue(e.target.value);
};
const handleEditSubmit = (e) => {
e.preventDefault();
if (editValue.trim()) {
onUpdate(todo.id, editValue.trim());
setIsEditing(false);
}
};
const handleDelete = () => {
onDelete(todo.id);
};
return (
<li className={`todo-item ${todo.completed ? 'completed' : ''}`}>
{isEditing ? (
<form onSubmit={handleEditSubmit} className="edit-form">
<input
type="text"
value={editValue}
onChange={handleEditChange}
className="edit-input"
autoFocus
/>
<button type="submit" className="save-button">
保存
</button>
</form>
) : (
<>
<input
type="checkbox"
checked={todo.completed}
onChange={handleToggle}
className="todo-checkbox"
/>
<span className="todo-text" onDoubleClick={handleEdit}>
{todo.text}
</span>
<button onClick={handleDelete} className="delete-button">
删除
</button>
</>
)}
</li>
);
}
export default TodoItem;TodoList 组件
jsx
// src/components/TodoList.js
import React from 'react';
import TodoItem from './TodoItem';
function TodoList({ todos, onToggle, onUpdate, onDelete }) {
return (
<ul className="todo-list">
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={onToggle}
onUpdate={onUpdate}
onDelete={onDelete}
/>
))}
</ul>
);
}
export default TodoList;TodoFooter 组件
jsx
// src/components/TodoFooter.js
import React from 'react';
function TodoFooter({ todos, onClearCompleted }) {
const completedCount = todos.filter(todo => todo.completed).length;
const totalCount = todos.length;
return (
<footer className="todo-footer">
<span className="todo-count">
已完成 {completedCount} / 总计 {totalCount}
</span>
{completedCount > 0 && (
<button onClick={onClearCompleted} className="clear-button">
清除已完成
</button>
)}
</footer>
);
}
export default TodoFooter;14.3 状态管理(useState 管理任务列表、输入框内容)
现在,我们需要在 App 组件中管理整体状态,包括任务列表的状态。
jsx
// src/App.js
import React, { useState, useEffect } from 'react';
import TodoHeader from './components/TodoHeader';
import TodoList from './components/TodoList';
import TodoFooter from './components/TodoFooter';
import './App.css';
function App() {
// 从本地存储中加载任务列表
const [todos, setTodos] = useState(() => {
const savedTodos = localStorage.getItem('todos');
return savedTodos ? JSON.parse(savedTodos) : [];
});
// 添加新任务
const handleAddTodo = (text) => {
const newTodo = {
id: Date.now(),
text,
completed: false
};
setTodos(prevTodos => [...prevTodos, newTodo]);
};
// 切换任务完成状态
const handleToggleTodo = (id) => {
setTodos(prevTodos =>
prevTodos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
// 更新任务内容
const handleUpdateTodo = (id, text) => {
setTodos(prevTodos =>
prevTodos.map(todo =>
todo.id === id ? { ...todo, text } : todo
)
);
};
// 删除任务
const handleDeleteTodo = (id) => {
setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
};
// 清除已完成任务
const handleClearCompleted = () => {
setTodos(prevTodos => prevTodos.filter(todo => !todo.completed));
};
// 保存任务列表到本地存储
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
return (
<div className="todo-app">
<TodoHeader onAddTodo={handleAddTodo} />
<TodoList
todos={todos}
onToggle={handleToggleTodo}
onUpdate={handleUpdateTodo}
onDelete={handleDeleteTodo}
/>
<TodoFooter
todos={todos}
onClearCompleted={handleClearCompleted}
/>
</div>
);
}
export default App;14.4 功能实现(添加、删除、修改、勾选任务)
14.4.1 添加任务
在 TodoHeader 组件中,我们通过表单提交来添加新任务。当用户输入任务内容并点击添加按钮时,会调用 onAddTodo 回调函数,将新任务添加到任务列表中。
14.4.2 标记任务完成/未完成
在 TodoItem 组件中,我们通过复选框的 onChange 事件来切换任务的完成状态。当用户点击复选框时,会调用 onToggle 回调函数,更新对应任务的 completed 属性。
14.4.3 编辑任务
在 TodoItem 组件中,我们通过双击任务文本进入编辑模式。在编辑模式下,用户可以修改任务内容,然后点击保存按钮或按 Enter 键来保存修改。
14.4.4 删除任务
在 TodoItem 组件中,我们添加了一个删除按钮。当用户点击删除按钮时,会调用 onDelete 回调函数,从任务列表中移除对应任务。
14.4.5 清除已完成任务
在 TodoFooter 组件中,我们添加了一个清除已完成按钮。当用户点击该按钮时,会调用 onClearCompleted 回调函数,过滤掉已完成的任务。
14.5 本地存储持久化(localStorage)
为了让任务列表在页面刷新后仍然保持,我们使用了 localStorage 来存储任务列表。
14.5.1 从本地存储加载数据
在 useState 的初始化函数中,我们从 localStorage 中读取保存的任务列表:
jsx
const [todos, setTodos] = useState(() => {
const savedTodos = localStorage.getItem('todos');
return savedTodos ? JSON.parse(savedTodos) : [];
});14.5.2 保存数据到本地存储
使用 useEffect 钩子,当 todos 状态变化时,将其保存到 localStorage:
jsx
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);14.6 样式优化(简单CSS美化,提升用户体验)
现在,我们来添加一些 CSS 样式,使 TodoList 应用更加美观。
css
/* src/App.css */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: Arial, sans-serif;
background-color: #f5f5f5;
color: #333;
}
.todo-app {
max-width: 600px;
margin: 50px auto;
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.todo-header {
padding: 20px;
background-color: #4CAF50;
color: white;
}
.todo-header h1 {
text-align: center;
margin-bottom: 20px;
}
.todo-input {
width: 80%;
padding: 10px;
border: none;
border-radius: 4px;
font-size: 16px;
}
.add-button {
width: 18%;
padding: 10px;
background-color: #2196F3;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
margin-left: 2%;
}
.add-button:hover {
background-color: #0b7dda;
}
.todo-list {
list-style: none;
}
.todo-item {
padding: 15px 20px;
border-bottom: 1px solid #eee;
display: flex;
align-items: center;
transition: background-color 0.2s;
}
.todo-item:hover {
background-color: #f9f9f9;
}
.todo-item.completed .todo-text {
text-decoration: line-through;
color: #999;
}
.todo-checkbox {
margin-right: 15px;
width: 18px;
height: 18px;
}
.todo-text {
flex: 1;
font-size: 16px;
cursor: pointer;
}
.delete-button {
background-color: #f44336;
color: white;
border: none;
border-radius: 4px;
padding: 5px 10px;
cursor: pointer;
font-size: 14px;
}
.delete-button:hover {
background-color: #d32f2f;
}
.edit-form {
flex: 1;
display: flex;
width: 100%;
}
.edit-input {
flex: 1;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.save-button {
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
padding: 8px 12px;
cursor: pointer;
font-size: 14px;
margin-left: 10px;
}
.save-button:hover {
background-color: #45a049;
}
.todo-footer {
padding: 15px 20px;
background-color: #f9f9f9;
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid #eee;
}
.todo-count {
font-size: 14px;
color: #666;
}
.clear-button {
background-color: #ff9800;
color: white;
border: none;
border-radius: 4px;
padding: 5px 10px;
cursor: pointer;
font-size: 14px;
}
.clear-button:hover {
background-color: #f57c00;
}
/* 响应式设计 */
@media (max-width: 600px) {
.todo-app {
margin: 20px;
}
.todo-input {
width: 70%;
}
.add-button {
width: 28%;
}
}14.7 完整代码讲解与优化建议
14.7.1 完整代码结构
我们已经创建了一个完整的 TodoList 应用,包括以下文件:
src/components/TodoHeader.js- 负责输入和添加新任务src/components/TodoItem.js- 显示单个任务,支持编辑和删除src/components/TodoList.js- 渲染任务列表src/components/TodoFooter.js- 显示任务统计和清除已完成任务src/App.js- 管理整体状态和逻辑src/App.css- 样式文件
14.7.2 优化建议
性能优化:
- 使用
React.memo包装组件,减少不必要的渲染 - 使用
useCallback缓存回调函数
- 使用
代码组织:
- 将本地存储逻辑提取到自定义 Hook 中
- 将样式模块化,使用 CSS Modules 或 styled-components
功能扩展:
- 添加任务过滤功能(全部、已完成、未完成)
- 添加任务排序功能
- 添加任务优先级设置
用户体验:
- 添加任务添加和删除的动画效果
- 添加键盘快捷键支持
- 优化编辑模式的用户体验
14.7.3 本地存储 Hook 示例
jsx
// src/hooks/useLocalStorage.js
import { useState, useEffect } from 'react';
export function useLocalStorage(key, initialValue) {
// 从本地存储中加载数据
const [value, setValue] = useState(() => {
try {
const savedValue = localStorage.getItem(key);
return savedValue ? JSON.parse(savedValue) : initialValue;
} catch (error) {
console.error('Error loading from localStorage:', error);
return initialValue;
}
});
// 保存数据到本地存储
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error('Error saving to localStorage:', error);
}
}, [key, value]);
return [value, setValue];
}使用自定义 Hook 优化 App 组件:
jsx
// src/App.js
import React from 'react';
import { useLocalStorage } from './hooks/useLocalStorage';
import TodoHeader from './components/TodoHeader';
import TodoList from './components/TodoList';
import TodoFooter from './components/TodoFooter';
import './App.css';
function App() {
const [todos, setTodos] = useLocalStorage('todos', []);
// 其他逻辑保持不变...
}
export default App;小结
本章通过实现一个完整的 TodoList 应用,我们巩固了以下 React 基础知识:
- 组件拆分和组合
- 状态管理(useState)
- 副作用处理(useEffect)
- 事件处理
- 表单操作
- 本地存储持久化
- CSS 样式设计
TodoList 是一个很好的入门项目,它涵盖了 React 开发中的许多常见场景。通过这个项目的实践,你应该对 React 的核心概念有了更深入的理解,为后续学习更复杂的 React 应用打下了基础。
