03_状态数据获取与表单
本地、URL、服务端状态的划分,请求取消、重复提交与表单反馈。
状态、数据获取与表单
学习目标:区分本地状态、URL 状态与服务端状态,处理并发请求、重复提交和表单失败。
1. 三类状态放对位置
| 状态 | 例子 | 合适位置 |
|---|---|---|
| 本地 UI 状态 | 弹窗开关、临时输入 | 组件或近邻状态 |
| URL 状态 | 搜索词、分页、选中标签 | 路由查询参数 |
| 服务端状态 | 笔记列表、详情 | 请求缓存与失效机制 |
服务端状态可能在别的设备或用户操作后改变;它有加载、错误、过期和重新获取问题,不宜简单复制到全局 store 后永久使用。
2. 请求生命周期
一个列表页至少明确:首次加载、空结果、请求失败、参数变化、重试、刷新和离开页面。搜索词快速变化时,旧请求可能晚于新请求返回;使用 AbortController、请求序号或数据获取库的机制防止旧结果覆盖新结果。
async function loadNotes(signal: AbortSignal): Promise<unknown> {
const response = await fetch('/api/notes', { signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json(); // 仍需运行时校验形状
}
不要对所有请求盲目重试,尤其是有副作用的写操作。需要重试创建时,服务端还应支持幂等策略。
3. 表单:浏览器与服务端双重校验
前端校验用于及时反馈,服务端校验用于保证规则;两者应共享字段约束的语义,但服务端仍是最终边界。提交时禁用或防抖按钮只是交互措施,不保证网络层只执行一次。
错误应尽量显示在对应字段附近,并让键盘用户能找到;成功后决定是保留输入、跳转详情还是更新列表。编辑页要考虑服务器版本已变化的冲突处理。
自测
- 为什么搜索词适合放 URL?刷新、分享链接和浏览器前进后退能保留状态。
- 禁用提交按钮能保证服务端只创建一次吗?不能,重试和并发请求仍可能重复。
4. 一次搜索请求的竞态
用户先搜索“go”,随后马上搜索“java”。若“go”请求晚返回,页面可能错误地显示旧结果。可以在参数变化时取消旧请求;如果客户端库或后端不支持可靠取消,还要用请求序号判断返回值是否仍属于当前条件。取消并不保证服务端已经停止工作,所以写入请求仍需服务端幂等保护。
let latestRequest = 0;
async function search(query: string, signal: AbortSignal) {
const requestId = ++latestRequest;
const response = await fetch(`/api/notes?q=${encodeURIComponent(query)}`, { signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data: unknown = await response.json();
if (requestId !== latestRequest) return; // 忽略过期响应
// 此处先做运行时校验,再更新界面状态。
}
这是说明竞态的模块级片段;真实应用应把请求序号放在对应组件或数据获取实例中,不能让无关页面共享一个全局计数器。
5. 表单状态与服务器冲突
编辑表单至少区分原始值、当前输入、字段错误和提交状态。用户提交时带上读取到的版本号;如果服务端返回 409,不应悄悄覆盖输入,而是提示刷新或比较差异。按钮禁用只能防一部分重复点击;网络重试、浏览器刷新仍可能重复提交,后端需按业务选择幂等键或唯一约束。
练习:在搜索框快速输入三个不同词,给 Mock API 设置反向返回顺序,确认页面最终只显示最后一次搜索结果。再模拟 409,检查表单内容是否保留。