React 实战: Todo 应用学习小结
🎯 项目完成情况
成功开发了一个功能完整的 Todo 应用,具备增删改查、状态切换和实时统计功能。
📚 核心概念掌握
1. useState - 状态管理
// 基础状态
const [taskInput, setTaskInput] = useState("");
const [tasks, setTasks] = useState([]);// 编辑相关状态
const [editingId, setEditingId] = useState(null);
const [editingText, setEditingText] = useState("");
关键理解:状态驱动 UI 更新,状态改变 → 组件重新渲染补充细节:
useState是一个 Hook,必须在函数组件的顶层调用,不能在条件语句、循环或嵌套函数中使用- 初始值只在组件第一次渲染时生效,后续渲染会被忽略
- 状态更新是异步的,React 可能会批量处理多个状态更新以提高性能
- 对于引用类型(对象、数组),直接修改不会触发重新渲染,必须创建新的引用
2. 数组状态更新模式(重要!)
// 添加:[...原数组, 新元素]
setTasks([...tasks, newTask]);// 删除:filter过滤
setTasks(tasks.filter(task => task.id !== id));// 修改:map遍历修改特定项
setTasks(tasks.map(task => task.id === id ? { ...task, 属性: 新值 } : task
));
补充细节:
- 这些方法都遵循了 "不可变数据" 原则,不会直接修改原数组
filter会返回一个新数组,包含所有符合条件的元素map会返回一个新数组,每个元素都是经过处理的原数组元素- 对于复杂数组操作,可以考虑使用
immer库简化代码 - 数组长度为 0 时(空数组),
filter会返回空数组,map不会执行回调函数
3. 事件处理
// 点击事件
onClick={() => handleFunction(param)}// 输入变化
onChange={(e) => setState(e.target.value)}// 键盘事件(React 18用onKeyDown)
onKeyDown={(e) => e.key === 'Enter' && handleSave()}
补充细节:
- React 事件是合成事件(SyntheticEvent),不是原生 DOM 事件,但拥有类似的 API
- 事件处理函数中的
this默认是undefined,所以通常使用箭头函数绑定上下文 - 传递参数时,使用箭头函数包裹是最常见的方式,如
onClick={() => handleDelete(id)} - 可以通过
e.preventDefault()阻止默认行为(如表单提交),e.stopPropagation()阻止事件冒泡 - 对于频繁触发的事件(如
onScroll、onResize),考虑使用防抖节流优化性能
4. 条件渲染
// 条件显示不同内容
{条件 ? 内容A : 内容B}// 根据状态添加CSS类
className={`基础类 ${条件 ? '特殊类' : ''}`}// 根据状态显示不同按钮
{editingId === task.id ? 编辑按钮组 : 正常按钮组}
补充细节:
- 除了三元表达式,还可以使用逻辑与
&&进行条件渲染:{条件 && 内容} - 逻辑与
&&渲染规则:条件为真时渲染后面的内容,条件为假时渲染false(React 会忽略false、null、undefined和true) - 可以将条件渲染逻辑抽取到函数中,提高代码可读性
- 对于复杂的条件渲染,可以使用变量存储要渲染的内容
- 避免在条件渲染中使用不同类型的元素作为同一位置的子元素,这会导致 React 重新创建 DOM 节点而非更新
🔧 具体功能实现
添加任务
const handleAddTask = () => {if (!taskInput.trim()) return;const newTask = {id: Date.now(), // 简单ID生成text: taskInput.trim(),completed: false // 默认未完成};setTasks([...tasks, newTask]);setTaskInput(""); // 清空输入框
};
补充细节:
Date.now()生成的 ID 在小型应用中足够用,但在并发添加任务时可能会有冲突- 更可靠的 ID 生成方案:可以使用
uuid库,或结合时间戳与随机数 trim()方法用于去除输入文本前后的空格,避免创建空任务或只有空格的任务- 可以添加任务重复检查,避免创建相同内容的任务
切换完成状态
const toggleTask = (id) => {setTasks(tasks.map(task => task.id === id ? { ...task, completed: !task.completed } : task));
};
// !操作符:取反布尔值
补充细节:
- 使用对象展开运算符
...task确保只修改completed属性,其他属性保持不变 - 状态更新是不可变的,我们没有直接修改原任务对象,而是创建了一个新对象
- 可以扩展此功能,添加完成时间记录:
{ ...task, completed: !task.completed, completedAt: !task.completed ? new Date() : null }
编辑任务流程
- 开始编辑:记录 id 和当前文本
const startEditing = (id, text) => {setEditingId(id);setEditingText(text);// 可以添加自动聚焦逻辑,提升用户体验 }; - 修改内容:更新 editingText 状态
<inputtype="text"value={editingText}onChange={(e) => setEditingText(e.target.value)}onKeyDown={(e) => {if (e.key === 'Enter') saveEditing(id);if (e.key === 'Escape') cancelEditing();}} /> - 保存:用 map 更新对应任务
const saveEditing = (id) => {if (!editingText.trim()) {// 可以删除空任务或提示用户return deleteTask(id);}setTasks(tasks.map(task => task.id === id ? { ...task, text: editingText.trim() } : task));setEditingId(null); }; - 取消:清空编辑状态
const cancelEditing = () => {setEditingId(null);setEditingText(""); };
🎨 组件结构理解
Todo组件
├── 输入区域 (状态: taskInput)
├── 任务列表 (状态: tasks)
│ ├── 任务项
│ │ ├── 任务内容 (条件渲染:编辑模式/显示模式)
│ │ └── 操作按钮 (条件渲染:编辑按钮组/正常按钮组)
└── 统计区域 (计算属性)
补充细节:
- 可以进一步拆分组件,提高复用性和可维护性:
TaskInput:负责任务输入和添加TaskList:负责任务列表展示TaskItem:负责单个任务项的渲染和操作TaskStats:负责统计信息展示
- 组件拆分原则:单一职责原则,一个组件只做一件事
- 父子组件通信通过 props 传递数据和回调函数
💡 需要深入理解的点
1. 为什么用函数更新状态?
// 可能有问题(闭包)
setTasks(tasks.filter(...));// 更安全(函数式更新)
setTasks(prevTasks => prevTasks.filter(...));
补充细节:
- 当状态更新依赖于前一个状态时,必须使用函数式更新
- 原因是 React 状态更新是异步的,多个连续更新可能会被合并
- 函数式更新确保我们总是基于最新的状态进行操作
- 对于复杂的状态依赖,函数式更新可以避免逻辑错误
- 示例:连续添加两个任务,使用函数式更新能确保第二个任务基于已添加第一个任务后的数组
2. key 属性的重要性
{tasks.map(task => (<div key={task.id}>...</div>
))}
- 帮助 React 识别元素
- 提高渲染性能
- 必须唯一且稳定补充细节:
- key 只需要在兄弟元素之间唯一,不需要全局唯一
- 避免使用数组索引作为 key,尤其是在数组会发生增删改的情况下,这会导致 React 错误地复用元素
- key 不会传递给组件内部,不能通过 props 访问
- 正确使用 key 可以避免不必要的 DOM 操作,提高性能
- 当 key 发生变化时,React 会销毁旧元素并创建新元素,这在需要重置组件状态时很有用
3. 事件处理中的箭头函数
// 每次渲染都创建新函数
<button onClick={() => handleClick(id)}>// 性能更好的方式(需要useCallback)
<button onClick={handleClick}>
补充细节:
- 内联箭头函数在每次渲染时都会创建一个新函数,可能导致子组件不必要的重渲染
- 使用
useCallback可以缓存函数引用,避免不必要的函数创建:const handleClick = useCallback((id) => {// 处理逻辑 }, []); // 依赖项数组为空时,函数只会创建一次 - 对于简单组件,性能差异可以忽略不计,优先考虑代码可读性
- 传递参数的另一种方式:使用数据属性(data-*)存储参数,在事件处理函数中通过
e.target.dataset获取
🚀 下一步学习方向
短期巩固
- 重新手写一遍代码(不看现成代码)
- 添加输入验证(如任务长度限制)
- 添加本地存储(localStorage)
// 保存到本地存储 useEffect(() => {localStorage.setItem('tasks', JSON.stringify(tasks)); }, [tasks]);// 从本地存储加载 const [tasks, setTasks] = useState(() => {const saved = localStorage.getItem('tasks');return saved ? JSON.parse(saved) : []; });
中长期进阶
- useEffect - 副作用处理
- useContext - 全局状态管理
- 自定义 Hooks - 逻辑复用
// 示例:自定义useLocalStorage hook function useLocalStorage(key, initialValue) {const [value, setValue] = useState(() => {const saved = localStorage.getItem(key);return saved ? JSON.parse(saved) : initialValue;});useEffect(() => {localStorage.setItem(key, JSON.stringify(value));}, [key, value]);return [value, setValue]; } - 性能优化 - React.memo, useCallback
📝 自我检查问题
- 我能独立写出添加任务的逻辑吗?
- 我理解 map 和 filter 在状态更新中的用法吗?
- 我知道条件渲染的几种写法吗?
- 我能解释为什么状态更新要用不可变方式吗?
💪 学习心得
从 "完全不会" 到 "基本实现",证明了:
- 分解问题:大功能拆成小步骤
- 逐步实现:一次只解决一个问题
- 调试技巧:console.log + 浏览器工具
- 不怕犯错:每个错误都是学习机会
记住:理解比记忆更重要,多实践才能真掌握!
下次复习时,尝试不看代码重新实现,看看哪些概念已经内化,哪些还需要加强。
