阶段7:实战项目 - TodoList
🎯 学习目标
整合前6个阶段的所有知识,开发一个功能完整的TodoList应用。
一、项目需求分析
功能清单:
- ✅ 添加任务: 输入框 + 添加按钮
- ✅ 显示列表: 展示所有任务
- ✅ 标记完成: 点击任务切换完成状态(带删除线)
- ✅ 删除任务: 每个任务有删除按钮
- ✅ 过滤显示: 全部/未完成/已完成
- ✅ 统计信息: 显示总数和完成数
涉及知识点:
- State管理(useState)
- 列表渲染(map)
- 条件渲染(三元运算符、&&)
- 事件处理(点击、输入、提交)
- 表单受控组件
二、完整代码 + 逐行注释
创建 src/TodoList.js:
import { useState } from 'react';
function TodoList() {
// ========== State定义 ==========
// 任务列表
const [todos, setTodos] = useState([
{ id: 1, text: '学习React基础', completed: false },
{ id: 2, text: '做TodoList项目', completed: false },
{ id: 3, text: '复习今天内容', completed: false }
]);
// 输入框的值
const [inputValue, setInputValue] = useState('');
// 当前过滤条件: 'all'(全部) | 'active'(未完成) | 'completed'(已完成)
const [filter, setFilter] = useState('all');
// ========== 功能函数 ==========
// 1. 添加任务
const handleAddTodo = (e) => {
e.preventDefault(); // 阻止表单提交刷新页面
// 验证:输入不能为空
if (inputValue.trim() === '') {
alert('请输入任务内容!');
return;
}
// 创建新任务对象
const newTodo = {
id: Date.now(), // 用时间戳作为唯一id
text: inputValue,
completed: false
};
// 添加到列表(放在最前面)
setTodos([newTodo, ...todos]);
// 清空输入框
setInputValue('');
};
// 2. 切换任务完成状态
const handleToggle = (id) => {
const newTodos = todos.map(todo => {
if (todo.id === id) {
// 找到目标任务,切换completed状态
return { ...todo, completed: !todo.completed };
}
return todo; // 其他任务不变
});
setTodos(newTodos);
};
// 3. 删除任务
const handleDelete = (id) => {
// 过滤掉要删除的任务
const newTodos = todos.filter(todo => todo.id !== id);
setTodos(newTodos);
};
// 4. 清空已完成任务
const handleClearCompleted = () => {
const newTodos = todos.filter(todo => !todo.completed);
setTodos(newTodos);
};
// ========== 计算属性 ==========
// 根据filter过滤任务
const filteredTodos = todos.filter(todo => {
if (filter === 'active') return !todo.completed; // 只显示未完成
if (filter === 'completed') return todo.completed; // 只显示已完成
return true; // 显示全部
});
// 统计数据
const totalCount = todos.length; // 总任务数
const activeCount = todos.filter(t => !t.completed).length; // 未完成数
const completedCount = todos.filter(t => t.completed).length; // 已完成数
// ========== 渲染UI ==========
return (
<div style={{
maxWidth: '600px',
margin: '40px auto',
padding: '20px',
backgroundColor: '#f5f5f5',
borderRadius: '10px',
boxShadow: '0 2px 10px rgba(0,0,0,0.1)'
}}>
{/* 标题 */}
<h1 style={{
textAlign: 'center',
color: '#333',
marginBottom: '30px'
}}>
📝 我的待办事项
</h1>
{/* ===== 输入区域 ===== */}
<form onSubmit={handleAddTodo} style={{ marginBottom: '20px' }}>
<div style={{ display: 'flex', gap: '10px' }}>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="输入新任务..."
style={{
flex: 1,
padding: '12px',
fontSize: '16px',
border: '2px solid #ddd',
borderRadius: '5px',
outline: 'none'
}}
/>
<button
type="submit"
style={{
padding: '12px 24px',
fontSize: '16px',
backgroundColor: '#4CAF50',
color: 'white',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
fontWeight: 'bold'
}}
>
添加
</button>
</div>
</form>
{/* ===== 过滤按钮 ===== */}
<div style={{
display: 'flex',
gap: '10px',
marginBottom: '20px',
justifyContent: 'center'
}}>
{['all', 'active', 'completed'].map(filterType => (
<button
key={filterType}
onClick={() => setFilter(filterType)}
style={{
padding: '8px 16px',
backgroundColor: filter === filterType ? '#2196F3' : '#e0e0e0',
color: filter === filterType ? 'white' : '#666',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
fontWeight: filter === filterType ? 'bold' : 'normal'
}}
>
{filterType === 'all' ? '全部' :
filterType === 'active' ? '未完成' : '已完成'}
</button>
))}
</div>
{/* ===== 任务列表 ===== */}
{filteredTodos.length === 0 ? (
// 没有任务时显示
<div style={{
textAlign: 'center',
padding: '40px',
color: '#999',
fontSize: '18px'
}}>
{filter === 'all' ? '还没有任务,快添加一个吧!' :
filter === 'active' ? '太棒了,没有待办任务!' :
'还没有已完成的任务'}
</div>
) : (
// 有任务时显示列表
<ul style={{
listStyle: 'none',
padding: 0,
margin: 0
}}>
{filteredTodos.map(todo => (
<li
key={todo.id}
style={{
backgroundColor: 'white',
padding: '15px',
marginBottom: '10px',
borderRadius: '5px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
transition: 'transform 0.2s',
}}
>
{/* 左侧:复选框 + 文字 */}
<div
style={{
display: 'flex',
alignItems: 'center',
flex: 1,
cursor: 'pointer'
}}
onClick={() => handleToggle(todo.id)}
>
{/* 自定义复选框样式 */}
<div style={{
width: '24px',
height: '24px',
borderRadius: '50%',
border: `2px solid ${todo.completed ? '#4CAF50' : '#ddd'}`,
backgroundColor: todo.completed ? '#4CAF50' : 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginRight: '12px',
flexShrink: 0
}}>
{todo.completed && (
<span style={{ color: 'white', fontSize: '16px' }}>✓</span>
)}
</div>
{/* 任务文字 */}
<span style={{
fontSize: '16px',
color: todo.completed ? '#999' : '#333',
textDecoration: todo.completed ? 'line-through' : 'none'
}}>
{todo.text}
</span>
</div>
{/* 右侧:删除按钮 */}
<button
onClick={() => handleDelete(todo.id)}
style={{
padding: '6px 12px',
backgroundColor: '#f44336',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px'
}}
>
删除
</button>
</li>
))}
</ul>
)}
{/* ===== 底部统计信息 ===== */}
<div style={{
marginTop: '20px',
padding: '15px',
backgroundColor: 'white',
borderRadius: '5px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<div style={{ color: '#666' }}>
<span style={{ fontWeight: 'bold', color: '#333' }}>
{activeCount}
</span>
{' '}个待完成
</div>
<div style={{ color: '#666' }}>
总共 <span style={{ fontWeight: 'bold', color: '#333' }}>
{totalCount}
</span> 个任务
</div>
{/* 清空已完成按钮(只有已完成任务时显示) */}
{completedCount > 0 && (
<button
onClick={handleClearCompleted}
style={{
padding: '6px 12px',
backgroundColor: '#ff9800',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px'
}}
>
清空已完成
</button>
)}
</div>
</div>
);
}
export default TodoList;
三、使用组件
在 src/App.js:
import TodoList from './TodoList';
function App() {
return <TodoList />;
}
export default App;
运行 npm start,打开浏览器查看效果!
四、功能演示
1. 添加任务
- 在输入框输入"学习JavaScript"
- 点击"添加"按钮或按回车
- 新任务出现在列表顶部
2. 标记完成
- 点击任务左侧的圆圈
- 任务文字变灰并加删除线
- 圆圈变绿并显示✓
3. 删除任务
- 点击任务右侧的"删除"按钮
- 任务从列表消失
4. 过滤显示
- 点击"未完成" → 只显示未完成任务
- 点击"已完成" → 只显示已完成任务
- 点击"全部" → 显示所有任务
5. 清空已完成
- 完成几个任务
- 点击底部"清空已完成"按钮
- 所有已完成任务被删除
五、代码结构解析
数据流示意图:
用户操作 → 触发事件处理函数 → 修改State → React重新渲染 → 页面更新
例如:
点击"添加"按钮
↓
handleAddTodo函数
↓
setTodos([新任务, ...旧任务])
↓
todos State变化
↓
React重新执行组件函数
↓
map()生成新的<li>元素
↓
页面显示新任务
关键技术点:
1. State设计
// 3个State变量,各司其职
const [todos, setTodos] = useState([...]); // 任务数据
const [inputValue, setInputValue] = useState(''); // 输入框
const [filter, setFilter] = useState('all'); // 过滤条件
2. 不可变更新
// ❌ 错误:直接修改数组(不会触发重新渲染)
todos.push(newTodo);
setTodos(todos);
// ✅ 正确:创建新数组
setTodos([newTodo, ...todos]);
// ✅ 正确:用map返回新数组
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
3. 过滤逻辑
// 先过滤,再渲染
const filteredTodos = todos.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true;
});
// 渲染过滤后的结果
{filteredTodos.map(todo => <li>...</li>)}
4. 条件渲染
// 没有任务时显示提示,有任务时显示列表
{filteredTodos.length === 0 ? (
<div>还没有任务...</div>
) : (
<ul>{filteredTodos.map(...)}</ul>
)}
// 只有已完成任务时才显示清空按钮
{completedCount > 0 && <button>清空已完成</button>}
六、扩展练习
难度1:添加编辑功能
// 提示:
// 1. 给todo添加isEditing属性
// 2. 双击任务进入编辑模式
// 3. 显示输入框,修改后保存
难度2:本地存储
// 提示:
// 1. 用localStorage保存todos
// 2. 页面刷新后数据不丢失
// 初始化时从localStorage读取
const [todos, setTodos] = useState(() => {
const saved = localStorage.getItem('todos');
return saved ? JSON.parse(saved) : [];
});
// todos变化时保存到localStorage
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
难度3:添加优先级
// 提示:
// 1. todo添加priority字段('高'/'中'/'低')
// 2. 用不同颜色显示
// 3. 可以按优先级排序
✅ 阶段7总结
你已经完成:
- ✓ 一个完整的TodoList应用
- ✓ 整合了所有React基础知识
- ✓ 理解了数据流和状态管理
- ✓ 掌握了实际项目的开发流程
项目涉及的知识点:
- ✅ useState管理状态
- ✅ 列表渲染(map)
- ✅ 条件渲染(三元、&&)
- ✅ 事件处理(点击、输入、提交)
- ✅ 表单受控组件
- ✅ 数组操作(filter、map)
- ✅ 不可变数据更新
验收标准:
- TodoList能正常运行
- 所有功能都能使用
- 理解每个函数的作用
- 能在此基础上添加新功能
下一步: 恭喜!你已经掌握了React的核心基础。下一阶段我们将了解后续学习方向,包括React Router、Hooks、状态管理库等进阶内容!
💡 调试技巧
1. 用console.log查看State
const handleAddTodo = (e) => {
e.preventDefault();
console.log('添加前:', todos); // 看看当前todos
setTodos([newTodo, ...todos]);
console.log('添加后:', [newTodo, ...todos]); // 看看新todos
};
2. 安装React Developer Tools
- Chrome/Edge扩展商店搜索"React Developer Tools"
- 安装后可以在浏览器查看组件State和Props
- F12打开开发者工具 → Components标签
3. 检查key警告
如果控制台出现"Each child should have a unique key prop"警告:
- 检查map()里是否加了key
- 确保key是唯一的