服务端状态不是全局变量
服务端状态不是全局变量
从接口拿到用户列表后,把它放进全局 Store,页面当然可以正常显示。麻烦发生在另一个组件也读取这份列表、用户资料刚被修改,或者页面重新获得焦点之后:内存里的数据到底还算不算当前状态?
这类数据由服务端拥有,浏览器里保存的只是某个时刻的快照。它可能被其他用户、后台任务或另一个标签页修改,也可能在请求返回前就失去意义。这是服务端状态与本地交互状态最重要的区别。
本地状态和服务端状态有不同问题
弹窗是否打开、当前选中了哪个标签、表单还没提交的输入,都由当前客户端决定。这些本地状态可以直接修改,通常也不需要考虑网络失败。
订单列表、用户资料和权限信息来自远端。读取它们会经历加载、成功或失败,成功结果还会随着时间变旧。客户端可以缓存和临时修改这份副本,但最终仍要与服务端重新确认。
把两者都放进同一个 Store 并非不能工作,只是团队要自己实现查询身份、新鲜度、并发合并、失败重试和失效传播。专门的查询状态层只是把这些反复出现的问题整理成统一模型。
查询键是服务端状态的地址
查询键应该包含决定结果的所有输入,例如资源类型、资源 ID、筛选条件、分页游标和租户。两个组件使用同一个键,才能订阅同一份数据;条件不同,就应该得到不同缓存。
键需要稳定,也需要能局部匹配。更新某个订单后,可以只让该订单详情和相关列表失效,而不是清空应用里的全部请求结果。键设计得过于随意,后续失效逻辑会到处依赖字符串拼接。
同一资源在列表和详情里可能有不同数据形状。是否将它们归一化成一份实体缓存,要看更新频率和一致性需求。简单项目直接让相关查询失效更容易理解;手工同步多份嵌套缓存虽然能立刻更新,却会增加遗漏风险。
成功之后还有新鲜和过期
查询不是只有 Loading、Success 和 Error。已经有数据但正在后台刷新,与第一次进入页面还没有任何内容,是两种不同体验。前者通常应该继续展示旧数据,只在局部表示正在更新。
数据在新鲜窗口内可以直接使用,超过窗口后则允许重新验证。过期不等于立刻删除,它只是表示下次合适的时机应该查询服务端。无人订阅的数据还可以继续保留一段时间,方便用户返回页面时快速显示。
窗口长短取决于业务。国家列表很少变化,支付结果和库存却更容易过期。缓存策略应表达用户能接受多旧的数据,而不是为了减少请求把所有内容都长期标记为新鲜。
更具体的查询键、缓存时间和并发去重关系见缓存、重试与请求去重。
更新完成后让相关查询重新确认
修改服务端数据后,最直接的做法是让受影响的查询失效。下一次刷新从权威来源取回结果,列表、详情和统计数据不会长期停留在不同版本。
如果接口已经返回完整的新对象,也可以直接更新对应缓存,再让其他派生查询失效。关键不是避免所有重新请求,而是明确哪些缓存能够从这次响应中可靠更新,哪些仍需要服务端重新计算。
无条件清空全部缓存虽然简单,却会让一次局部修改触发大量无关请求。反过来,试图精确手改每一个相关字段,也可能让失效逻辑比业务本身更难维护。
乐观更新是在借用未来的成功结果
点赞、排序和开关操作通常希望立即反馈。乐观更新会先修改客户端缓存,再向服务端提交;如果请求失败,就恢复之前的快照。
这套流程需要先取消可能覆盖当前缓存的旧查询,保存回滚数据,并在操作结束后重新验证。多个乐观更新同时发生时,回滚顺序也可能互相影响。服务端如果会重新计算结果,客户端提前猜出的状态未必与最终结果一致。
因此,乐观更新更适合结果容易预测、失败概率较低、回滚代价不大的操作。付款、库存扣减或权限变更即使界面需要快速反馈,也不适合假装服务端已经确认。
分页不是几份互不相关的数组
分页查询的身份包含筛选和游标。筛选条件改变后,旧页不能继续拼到新结果里;删除一项后,后续页的边界也可能变化。
基于页码的接口容易跳转,数据插入和删除时却可能重复或遗漏;游标更适合持续变化的数据,但游标通常只在原查询条件和排序下有效。客户端要保留这些语义,不能把游标当成普通页码计算。
切换下一页时,可以暂时显示上一页数据来避免界面闪空,但要让用户知道当前正在加载新页,旧内容也不能被误认为新查询已经完成。
服务端渲染要交接缓存所有权
SSR 可以在服务端先获取查询结果,再把必要状态交给浏览器继续使用。服务端缓存必须按请求或用户隔离,不能让带身份的数据进入跨用户共享实例。
浏览器接管后还要判断这份数据何时获取、是否已经过期,以及是否需要马上重新验证。服务端和客户端时钟、序列化限制及重复请求都会影响这次交接。Hydration 的目标不是永久信任首屏数据,而是让客户端从同一份快照继续运行。
服务端状态管理工具可以减少样板代码,但无法替业务决定什么算同一份数据、允许过期多久、哪些更新可以乐观执行。把这些规则说清楚,比把所有接口结果搬进一个全局 Store 更重要。