跳到主要内容

阶段7:实战项目 - TodoList

🎯 学习目标

整合前6个阶段的所有知识,开发一个功能完整的TodoList应用。


一、项目需求分析

功能清单:

  1. 添加任务: 输入框 + 添加按钮
  2. 显示列表: 展示所有任务
  3. 标记完成: 点击任务切换完成状态(带删除线)
  4. 删除任务: 每个任务有删除按钮
  5. 过滤显示: 全部/未完成/已完成
  6. 统计信息: 显示总数和完成数

涉及知识点:

  • 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总结

你已经完成:

  1. ✓ 一个完整的TodoList应用
  2. ✓ 整合了所有React基础知识
  3. ✓ 理解了数据流和状态管理
  4. ✓ 掌握了实际项目的开发流程

项目涉及的知识点:

  • ✅ 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是唯一的