第一个交互应用
第一个交互应用 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
前面九课的零件都齐了,这一课把它们装成第一个能独立运行的小应用:课程收藏列表。新增、删除、筛选、状态恢复一个不缺,边界情况全部覆盖。读完你应当有能力独立完成一个 CRUD 小工具,而不只是看懂别人的代码。
问题场景
需求很朴素:输入课程名收藏它;不想收藏了就删;列表长了要能按「全部 / 已收藏」切换;刷新页面后收藏还在。动手前先想清楚三个问题:数据模型是什么?有哪些交互、各自改哪块数据?哪些状态该恢复、怎么恢复?
答案是:数据模型就用一个数组 [{ id, title, done }];四个交互分别对应数组的增、删、改(done 标记)和筛选;恢复用 localStorage 存这个数组——组件结构反过来从数据模型推导。
核心解释
一、先立数据模型,再写组件。 列表应用的所有交互都是对同一个数组的操作,把它定稳了,组件树自然清晰:App 持有数组(状态提升给筛选控件用,R14 会展开讲),AddForm 收集输入,FilterTabs 显示切换,CourseItem 渲染单条。
二、每个交互都是一次「不可变更新」。 新增和删除在 R06/R13 都见过雏形,这里的关键是别在事件处理器里直接改数组:
function addCourse(title) {
setCourses([...courses, { id: crypto.randomUUID(), title, done: false }]);
}
function removeCourse(id) {
setCourses(courses.filter((c) => c.id !== id));
}
新数组交给 setState,React 才感知到变化;id 用 crypto.randomUUID() 现场生成(R06 的稳定标识)。
三、边界情况决定应用质感。 空输入不添加(trim() 后判空);重复收藏提示而不是塞进列表;全部删光后给空状态界面;筛选结果为空时说明「这个筛选下没有数据」而不是空白。
四、状态恢复只恢复数据,不恢复 UI 细节。 页面加载时从 localStorage 读数组还原;当前选中哪个筛选 tab 属于「会话内 UI 状态」,通常不用恢复。写入时机选在数据变化之后:
useEffect(() => {
localStorage.setItem("faved-courses", JSON.stringify(courses));
}, [courses]);
示例拆解
实验区就是完整的收藏列表应用,关键点如下:
- 数据流单行成立:
AddForm只上报输入,FilterTabs只上报筛选值,全部状态在App集中管理; - 筛选不用存第二份数组:
filter是一个 state,列表是courses.filter(...)的派生结果(这个思路 R11 会正式展开); - localStorage 的读写都包了 try/catch——沙箱或隐私模式下存储可能不可用,降级成内存模式,应用照样能跑;
- 空列表、空输入、重复项三个边界都有专属提示文案。
验证方式:添加两条、删除一条、切筛选、刷新沙箱(或重开实验),数据应当还原。
动手任务
打开本课实验区,完成三个小改动:
- 加「清空全部」按钮,弹出确认(用
confirm)后再删。 - 给课程项加「取消收藏」标记按钮(切换
done字段),已收藏项显示对勾。 - 把「已收藏」筛选改成按
done字段过滤,而不是仅靠存在性。
常见错误
- 新增时直接
courses.push(...)再 setState(courses):数组引用没变,React 不更新;每次都返回新数组。 - 存进 localStorage 的是 JSX 或含函数的对象:
JSON.stringify会丢掉不可序列化的部分,只存纯数据。 - 读取 localStorage 后忘了处理损坏数据:
JSON.parse抛异常让应用白屏;try/catch 解析,失败就回退初始数据。 - 用输入框的 DOM 值直接当数据:受控输入(R15 会展开)让 React state 始终是唯一事实源,别从 DOM 里捞值。
检查与复盘
完成下方检查题确认理解。自查清单:
- 能先写出数据模型和交互清单,再动手写组件
- 四个数组操作(增删改筛)都能用不可变方式一口气写对
- 知道哪些状态要恢复、哪些不恢复,读写 localStorage 都有降级方案
参考资料
- React 官方文档:状态管理 —— state 与组件记忆的官方综述
- React 官方文档:用 Effect 同步 —— 与外部系统(localStorage)同步的写法
读完这一课了?
标记完成后,工作台会安排接下来的学习。