服务端数据与界面状态
服务端数据与界面状态 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
读完你能把一个页面的状态按来源分成三类——远程数据、筛选条件、临时选择——给每类安排合适的存放位置和更新节奏,消除「刷新数据把筛选和选择覆盖了」「两份缓存互相打架」这类问题。
问题场景
课程列表页第一版把所有东西塞进一个 useState 对象:
const [page, setPage] = useState({ courses: [], keyword: "", selected: [], loading: false });
问题接连出现:点「刷新」后 selected 被清空;两个组件各自拉了一份数据,筛选后互相对不上;想保留筛选条件刷新一下页面,全没了。根因是不同生命周期的状态被绑在同一个对象里,同生共死。
核心解释
先分类,再安排:
- 远程数据(courses):来自服务端,生命周期跟「请求动作」走——只有请求结果可以写它,筛选、选择都不许直接碰;
- 筛选条件(keyword、排序):用户意图,生命周期跟「页面会话」走,刷新数据时必须原样保留,适合放 URL 参数或提升到公共父组件;
- 临时选择(selected、展开行):纯界面状态,生命周期跟「用户操作」走,通常留在使用它的组件里。
分类之后,数据流变成单向管道,展示用的列表是派生值,不进 state:
const [keyword, setKeyword] = useState(""); // 筛选条件:独立 state
const [courses, setCourses] = useState(null); // 远程数据:只有 fetch 结果写它
const [selected, setSelected] = useState(() => new Set()); // 临时选择:独立 state
const visible = useMemo(
() => (courses || []).filter((c) => c.title.includes(keyword)),
[courses, keyword] // 展示列表 = 远程数据 × 筛选条件,现算
);
「刷新数据」只调 setCourses,keyword 与 selected 原封不动;「改筛选」只动 keyword,不触发重新请求(除非筛选属于服务端参数——那就让它成为缓存键的一部分,下一课展开)。避免重复缓存的口诀:同一份数据只存一次,其余全是派生;两处存同一份数据,迟早不一致。验证方式:刷新、筛选、选择三个操作各做一轮,确认每一步只有一个类别的值在变。
示例拆解
实验区课程列表带搜索框、刷新按钮和多选:
- 顶部显示「已选数量」与状态写入日志,每次 state 被写都留痕;
- 点「刷新课程数据」:600ms 后 courses 更新,keyword 与 selected 保持不变,日志只记一条「写 courses」;
- 输入关键词:列表即时变化,不发请求、不动 courses;
- 勾选课程:只动 selected;
- 验证:交替做三个操作各两轮,检查日志——每步只应有一个类别被写入。
动手任务
打开本课实验区:
- 把 selected 改成「刷新时清空」(业务上合理的选择),写出你在哪里加的代码,并说明为什么不能在 setCourses 的同一行顺手清。
- 新增「只看 8 小时以上」开关,判断它属于哪一类状态,加到正确的位置。
- 故意把 courses 复制一份存进另一个 state 并在筛选时同步修改,制造一次不一致;再改回单源,总结教训。
常见错误
- 把派生列表也存进 state:筛选结果、排序结果都是计算值,存了就多一个要同步的副本;用 useMemo 现算。
- 刷新逻辑顺手重置其他状态:「刷新数据」和「回到初始筛选」是两个动作,混在一起用户的选择就丢了。
- 写入时改远程数据的形状:请求层返回什么就存什么,裁剪、重排留给派生层,否则两处消费对不上。
- 临时状态提升过高:selected 只有一个组件用,却放进全局,别的更新跟着重渲染;提升到「需要的最小公共祖先」为止。
检查与复盘
自查清单:
- 能对页面每个 state 说出类别(远程 / 筛选 / 临时)和生命周期
- 刷新、筛选、选择三个操作互不干扰,验证过
- 同一份数据在页面上只有一处存储
参考资料
- React 官方文档:选择状态结构 —— 状态分类与结构原则
- React 官方文档:管理状态 —— 状态归属的完整决策树
- React 官方文档:组件间共享状态 —— 状态该提升到哪一层
读完这一课了?
标记完成后,工作台会安排接下来的学习。