前端状态管理实战:从组件状态到服务端缓存
本文目录
依据跨框架、React、Vue、小程序、Angular 六类资源,讲清 UI 状态、URL 状态、服务端缓存、SSR 和跨标签同步的边界。
本文围绕 tnfe/awesome-state 做原创工程整理。项目来自 腾讯新闻前端团队 2022 年的 25 个 Awesome 项目推荐;上游 README 固定到 commit 0bc463101b11fef478d448288a4193c6b552362d(2022-12-02);含六个框架分类与约 103 条资源链接行,仓库没有声明 SPDX 许可。 下方保留固定版本入口。
仓库内容地图
- 跨框架:可观察对象、不可变数据和事件驱动的共性
- React:局部状态、Context、外部 Store 与异步服务端状态
- Vue:响应式系统、组合式函数和集中式 Store
- 小程序:页面栈、全局数据与跨页面通信
- Angular:服务注入、响应式流和大型应用组织方式
- 其他:状态机与表单状态不应和通用 Store 混为一谈
怎么读才有收获
上游固定快照发布于 2022-12-02,目录包括 cross framework、React、Vue、mini-program、Angular 和 other。先判断状态的权威来源、共享范围与生命周期,再选择组件 state、URL、store 或 query cache。
状态管理先解决边界,再决定用什么库
“状态”不是同一种数据。一个页面的输入框、筛选条件、跨路由购物车、后端返回的商品、当前 URL、登录会话和跨标签通知,生命周期、可信来源、同步策略完全不同。把它们统统塞进一个全局 store,通常会造成重复缓存、隐式耦合和退出登录后旧数据残留。
先给每个字段回答五个问题:谁是权威来源?谁会读取?需要活多久?刷新/返回时要不要保留?多个写入者冲突时听谁的?答案决定它应该留在组件、URL、服务端缓存、跨窗口同步层还是服务器,而不是先决定 Redux、Pinia 或 Zustand。
1. 把状态分成六类
| 状态类别 | 常见例子 | 首选归属 | 典型错误 |
|---|---|---|---|
| 临时交互 | hover、弹窗、当前输入草稿、展开项 | 组件本地状态 | 为一个组件的开关建立全局 store |
| 可分享导航 | 搜索词、筛选、排序、页码、当前 tab | URL path/query/hash | 只存在内存,复制链接或浏览器返回就丢 |
| 复杂工作流 | 多步骤表单、编辑器、上传任务、对话状态 | reducer / 有限状态机 / 领域 store | 用多个布尔值组合出非法状态 |
| 共享客户端状态 | 当前选中的工作区、跨页面临时草稿、应用偏好 | 最近公共父组件、Context、Pinia 或轻量 store | 复制字段后让多个组件各自维护 |
| 服务端状态 | 用户资料、文章、列表、权限结果、分页数据 | Query cache / API 层 | 同一响应同时存在全局 store、组件和 query cache |
| 持久和跨端事实 | 订单、权限、书架进度、账号设置 | 服务端数据库;浏览器只做可失效缓存 | 把 localStorage 当授权或最终真相 |
派生值通常不应再次存储。例如总价从商品与数量计算,筛选结果由原数组和条件得出,能在渲染时或 selector 中稳定计算就不要再维护一份可过期副本。React 官方状态指南特别提醒重复状态容易产生 bug,并建议先决定每个状态的单一所有者。
2. 共享范围决定状态放在哪里
只被一个视图使用的值留在组件。兄弟组件需要同步时提升到最近公共父组件,并把数据与事件回调传下去。深层多处分发的只读环境值可用 React Context;它是依赖注入和树级共享机制,不是自动提供 selector、持久化、请求缓存、事务或跨标签一致性的完整状态平台。复杂转移可用 reducer 集中表达事件和下一状态。
当同一份客户端领域状态被许多无关页面读写、需要订阅选择器、开发工具或可控中间件时,再引入 store。Redux Toolkit 适合需要明确 action、复杂更新规则和调试追踪的团队;Zustand 提供轻量 selector/store 模型;Vue 项目优先按 Vue 响应式与 Pinia 组织。它们不是逐项等价替换,先定义领域边界、SSR 模型、测试能力和团队约束再选。
3. 服务端数据单独管理缓存语义
服务端数据不只是“一个对象”:它还有加载中、旧数据、刷新中、失败、重试、分页、失效和并发订阅等状态。Query 类工具专门处理这些生命周期。若在 Redux 应用里使用 Redux Toolkit,官方把 RTK Query 作为常见获取/缓存场景的默认方案;TanStack Query 则提供独立 query cache;两者都不取代服务端授权。
缓存 key 必须包含决定响应身份的筛选和租户/账号上下文,但 key 不是访问控制。退出登录、切换账号或租户时,要取消进行中的私有请求并清除旧账号缓存;服务器仍要逐请求做资源级授权。写操作后优先按标签/查询失效重取;乐观更新要有回滚策略,并处理请求失败和并发写入。
SSR 中尤其不能把带用户数据的 QueryClient 放在服务器模块级单例中,否则并发请求可能共享缓存。每个请求创建独立缓存,只把允许公开给当前用户的序列化数据注入 HTML;CDN 对个性化页面设置私有缓存策略。首屏关键数据和次要数据也应区分,避免所有请求都阻塞渲染。
4. 异步竞态:取消与身份校验都要有
搜索框快速输入时,旧请求可能晚于新请求返回。只设置 loading 不足以防止旧结果覆盖新条件。优先用支持 AbortSignal 的查询层取消已过期请求;如果异步工作不可取消,为每次请求保存单调递增的 requestId,仅接受当前 ID 的结果。路由切换、账号切换、组件卸载都应定义取消或忽略策略。
function searchReducer(state, action) {
switch (action.type) {
case 'search/started':
return { ...state, requestId: action.requestId, status: "loading", error: null };
case 'search/succeeded':
if (action.requestId !== state.requestId) return state;
return { ...state, status: "ready", results: action.results };
case 'search/failed':
if (action.requestId !== state.requestId) return state;
return { ...state, status: "error", error: action.error };
default:
return state;
}
}
这个 reducer 示例把合法状态转移集中起来,但不替代请求取消、错误分类、输入防抖和无障碍状态提示。重要的是 stale response 不会改写当前查询,而不是把所有竞态隐藏在组件 effect 里。
5. React、Vue 和跨框架目录怎样读
固定上游目录分为 cross framework、React、Vue、mini-program、Angular、other 六类。cross framework 部分可以用于理解 action/reducer、响应式依赖、事件流和状态机等设计谱系;React 部分重点区分局部 state、提升状态、Context 与外部 store;Vue 部分要看响应式状态、store 组合和 SSR hydration;小程序要验证页面/组件生命周期、分包、平台 API 和跨页通信;Angular 要把服务提供范围与 DI 生命周期一起考虑;other 部分作为发现入口而不是质量排序。
React 官方指南提供从组件本地 state、提升状态到 reducer/context 的渐进路径。Pinia 将 store 分成 state/getters/actions,并要求声明初始字段;Zustand 以 selector 订阅 store;Redux Toolkit 强调规范化更新和工具链。读任何方案时都应查清:更新如何触发订阅、selector 是否稳定、状态能否序列化、开发工具记录什么、SSR 如何隔离、卸载后订阅由谁清理。
6. URL、刷新与跨标签同步
可复制、可收藏、可后退的筛选状态应尽量进入 URL,并做严格解析与默认值规范化。表单未提交草稿则通常留在视图;确实要刷新恢复时,使用按用户/草稿 ID 隔离、带版本号和过期时间的本地草稿。不要把 token、支付信息、私密表单或权限凭据写入 query string、持久化插件或公开埋点。
跨标签页可以用 BroadcastChannel 发送“数据可能变化”的失效通知,再由每个标签从服务器重新取权威数据;兼容场景可用 storage event 传递轻量版本号。不要把访问令牌、完整用户对象或秘密放进广播消息。要处理当前标签不接收自己的 storage 事件、消息重复、标签休眠、版本冲突和登出时的清理;客户端同步不能代替服务端授权。
7. 持久化与安全
持久化采用 allowlist,只保存明确需要跨刷新保留的字段;加 schema version 并写迁移、损坏数据回退和过期清理。不要把整个 store 自动 JSON.stringify 到 localStorage:它可能包括临时错误、旧账号数据、超大响应或秘密。浏览器存储可被同源脚本读取,不能用它证明用户身份;涉及授权的事实一律由服务端会话和数据库判定。
乐观更新和本地草稿还要考虑多标签并发、设备离线、服务端冲突与账号退出。对关键修改采用版本号/ETag 或服务端冲突检测;对高价值业务不要只做“最后写入者胜”。把状态机事件记入审计日志时也要做隐私最小化,避免将整份表单或 token 写入事件历史。
8. 选型矩阵
| 需求 | 先考虑 | 升级触发条件 | 验收重点 |
|---|---|---|---|
| 单组件交互 | React useState / Vue ref | 多视图必须协调同一事实 | 状态重置、组件卸载、派生值 |
| 可分享筛选/分页 | URL query + router | 需要复杂校验或路由数据预加载 | 直接访问、后退/前进、刷新、非法参数 |
| 深层只读环境/少量共享状态 | React Context / Vue provide-inject | 高频细粒度订阅或跨树复用增加 | 订阅范围、默认值、依赖生命周期 |
| React 客户端领域状态 | Zustand 或 Redux Toolkit | 团队需要更明确的 action、审计/调试或复杂状态转移 | selector 渲染、序列化、账号切换、测试 |
| Vue 共享客户端状态 | Pinia | 多 store 协作、SSR、插件和 DevTools 需求增加 | hydration、store 生命周期、插件副作用 |
| 后端查询与缓存 | TanStack Query / RTK Query | 请求去重、重试、失效、分页和后台刷新变复杂 | 缓存 key、并发、失效、退出登录清理 |
| 多步业务工作流 | reducer 或显式状态机 | 状态组合产生非法状态或需要恢复/审计 | 事件覆盖、非法转移、超时与重试 |
不要为了追求“全能”同时装多个全局 store 和多个服务端缓存库。可以用 URL 负责导航事实、query cache 负责后端数据、一个轻量领域 store 负责确有共享价值的客户端状态,剩下留在组件。
9. 测试清单与渐进迁移
- 单一事实:派生值不重复存储,更新入口有明确所有者。
- 状态转移:成功、失败、取消、重复事件和过期响应都有断言。
- 缓存隔离:不同用户/租户的 key 不串,登出清除私有缓存。
- URL:刷新、分享、浏览器后退、非法 query 和默认值均稳定。
- SSR:并发请求不共享 QueryClient;HTML 不泄露其他用户的数据。
- 持久化:schema 升级、损坏数据、过期草稿和存储不可用有回退。
- 多标签:重复广播、标签休眠、登出和冲突不会恢复旧权限状态。
- UI:加载/空/失败/旧数据/刷新状态均可理解且可访问。
迁移时先盘点重复来源,选一个代表页面做状态归属图;把服务端列表迁到 query 层,把可分享筛选迁到 URL,再把确需跨页共享的客户端领域状态抽到 store。每一步保留旧/新数据的一致性断言和回滚点,不要先建一个全局万能 store 再把所有字段搬进去。
官方文档
- React:Managing State
- React:Sharing State Between Components
- Redux:Style Guide
- Redux Toolkit:RTK Query Cache Behavior
- Pinia:State
- Zustand:Introduction
- TanStack Query:Server Rendering and Hydration
上游快照说明
该项目 README 固定到 2022-12-02 的 commit 0bc463101b11fef478d448288a4193c6b552362d,含 cross framework、React、Vue、小程序、Angular、other 六个方向和约 103 条资源链接行。GitHub API 未声明 SPDX 许可,根目录也无 LICENSE;本文不复制原始库名/介绍清单,而是按原分类结构写成原创状态架构指南。条目可能已停止维护,必须逐项核对当前状态。
动手路线
- 以搜索页实现 URL 筛选、请求缓存和组件临时状态三层,确保刷新、分享和返回可用。
- 模拟快速查询、账号切换和登出,验证旧响应不会覆盖新状态,私有缓存不会串号。
- 做一次 SSR 并发请求与跨标签失效测试,检查缓存隔离、授权和持久化清理。
使用边界
仓库未声明 SPDX 许可证且根目录没有 LICENSE;本文不复制原始 103 条资源链接说明。目录是 2022 年的发现快照,不代表依赖仍维护,使用前需核对当前文档、发布和许可证。
原始入口
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。