State Management (Deep Workflow)
Most “state bugs” are ownership and lifecycle bugs: who writes, when it syncs, and what happens on failure. Guide users to explicit models instead of ad-hoc globals.
When to Offer This Workflow
Trigger conditions:
- - Prop drilling pain, inconsistent UI, duplicate sources of truth
- Stale data after mutations; optimistic UI gone wrong
- Choosing a library vs Context vs server-state library (React Query, SWR, Apollo)
- SSR/hydration + client state mismatches
Initial offer:
Use six stages: (1) inventory state kinds, (2) assign ownership, (3) server vs client boundaries, (4) async & updates, (5) persistence & URLs, (6) testing & DevTools. Confirm framework and data fetching approach.
Stage 1: Inventory State Kinds
Goal: Classify what state exists before picking tools.
Categories
- - Remote/server state: API responses, pagination, staleness—often not Redux-shaped
- URL state: filters, tabs, selection when shareable/bookmarkable
- Session UI state: modals, toggles, transient form drafts
- Client-derived: sorted/filtered views of remote data—avoid storing both raw and derived as writable truths
- Global cross-cutting: auth user, theme, feature flags—with clear read-only vs mutable rules
Exit condition: Table of state slices → source of truth → consumers.
Stage 2: Assign Ownership
Goal: One writer per piece of truth (or strict reducer pattern).
Rules of Thumb
- - Colocate state where it’s used if not shared—don’t globalize prematurely
- Lift only when multiple subtrees need it or prop chains hurt and ownership is clear
- Avoid copying server entities into multiple stores without sync rules
Exit condition: For each slice: who sets, who reads, invalidation story.
Stage 3: Server vs Client Boundaries
Goal: Prefer server-state libraries for remote data; use client stores for true client concerns.
Guidance
- - React Query / SWR / RTK Query: caching, dedupe, refetch, background refresh—use for HTTP JSON
- GraphQL clients: normalized cache + mutations with update policies
- Redux: great for predictable global client state + middleware—not every fetch response
Anti-patterns
- - Storing full API JSON in Redux without normalization when Apollo/Query would fit
- Double fetch: SSR then client refetch without coordination—use hydration patterns
Exit condition: Remote data has defined cache keys, stale time, and mutation flow.
Stage 4: Async & Consistency
Goal: Loading, error, empty, and retry are first-class; optimistic updates are safe or scoped.
Practices
- - Optimistic UI: rollback path; idempotent mutations; server reconciliation
- Ordering: serial vs parallel requests; race cancellation (AbortController)
- Pagination/infinite scroll: cursor stability; dedupe pages
Concurrency
- - Last write wins awareness; versioning or ETags if server supports
Exit condition: User-visible failure modes handled; no silent stale success.
Stage 5: Persistence & URL
Goal: Decide what survives refresh, what is shareable, and security.
Options
- - URL query for filters/tabs when users share links
- localStorage/sessionStorage for drafts—mind XSS risk (sensitive data → not localStorage)
- IndexedDB for offline—complexity tax
Exit condition: Sensitive data never persisted client-side inappropriately.
Stage 6: Testing & DevTools
Goal: State logic is unit-testable; time-travel/debug when needed.
Practices
- - Pure reducers / small hooks for rules
- Integration tests for critical flows with MSW or mock server
- DevTools: Redux/Query panels—ensure team knows how to inspect cache
Final Review Checklist
- - [ ] State classified: remote vs UI vs URL vs derived
- [ ] Single owner per truth; invalidation clear
- [ ] Server cache strategy chosen for API data
- [ ] Async/error/empty/optimistic paths specified
- [ ] Persistence/security boundaries respected
Tips for Effective Guidance
- - Start minimal: Context + server-state library solves many “Redux-sized” problems.
- Always ask: “What is the source of truth after mutation returns?”
- Mention React Strict Mode double-invoke when debugging weird effect behavior.
Handling Deviations
- - Legacy Redux everywhere: incremental migration—feature modules first, remote data to Query next.
- Non-React: map patterns to signals/stores (Vue Pinia, Svelte stores) with same ownership discipline.
状态管理(深度工作流程)
大多数状态错误本质上是所有权和生命周期错误:谁写入、何时同步、失败时如何处理。引导用户采用显式模型,而非临时全局变量。
何时提供此工作流程
触发条件:
- - Prop 传递困难、UI 不一致、多数据源冲突
- 变更后数据陈旧;乐观更新出错
- 选择库 vs Context vs 服务端状态库(React Query、SWR、Apollo)
- SSR/水合 + 客户端状态不匹配
初始建议:
使用六个阶段:(1) 盘点状态类型,(2) 分配所有权,(3) 服务端与客户端边界,(4) 异步与更新,(5) 持久化与 URL,(6) 测试与 DevTools。确认框架和数据获取方式。
阶段 1:盘点状态类型
目标: 在选择工具前,分类存在哪些状态。
分类
- - 远程/服务端状态:API 响应、分页、过期状态——通常不是 Redux 形态
- URL 状态:可分享/可书签化的筛选器、标签页、选择项
- 会话 UI 状态:模态框、开关、临时表单草稿
- 客户端派生状态:远程数据的排序/筛选视图——避免将原始数据和派生数据都存储为可写数据源
- 全局横切状态:认证用户、主题、功能开关——明确只读与可写规则
退出条件: 状态切片 → 数据源 → 消费者 的表格。
阶段 2:分配所有权
目标: 每个数据源只有一个写入者(或严格的 reducer 模式)。
经验法则
- - 如果状态不被共享,就近存放——不要过早全局化
- 仅当多个子树需要或 prop 链造成困扰且所有权明确时,才提升状态
- 避免在没有同步规则的情况下,将服务端实体复制到多个存储中
退出条件: 每个切片明确:谁设置、谁读取、失效策略。
阶段 3:服务端与客户端边界
目标: 远程数据优先使用服务端状态库;真正的客户端关注点使用客户端存储。
指导建议
- - React Query / SWR / RTK Query:缓存、去重、重新获取、后台刷新——适用于 HTTP JSON
- GraphQL 客户端:规范化缓存 + 带更新策略的变更操作
- Redux:适用于可预测的全局客户端状态 + 中间件——并非每个获取响应都需要
反模式
- - 在 Apollo/Query 更合适的情况下,将完整 API JSON 未经规范化存入 Redux
- 双重获取:SSR 后客户端未经协调重新获取——使用水合模式
退出条件: 远程数据具有定义的缓存键、过期时间和变更流程。
阶段 4:异步与一致性
目标: 加载、错误、空状态和重试是一等公民;乐观更新安全或限定范围。
实践
- - 乐观 UI:回滚路径;幂等变更;服务端协调
- 排序:串行与并行请求;竞态取消(AbortController)
- 分页/无限滚动:游标稳定性;页面去重
并发
- - 最后写入获胜意识;如果服务端支持,使用版本控制或 ETag
退出条件: 用户可见的失败模式已处理;无静默的陈旧成功状态。
阶段 5:持久化与 URL
目标: 决定什么在刷新后保留、什么可分享以及安全性。
选项
- - 用户分享链接时,筛选器/标签页使用 URL 查询参数
- 草稿使用 localStorage/sessionStorage——注意 XSS 风险(敏感数据 → 不使用 localStorage)
- 离线使用 IndexedDB——复杂度代价
退出条件: 敏感数据绝不不恰当地在客户端持久化。
阶段 6:测试与 DevTools
目标: 状态逻辑可单元测试;需要时可时间旅行/调试。
实践
- - 规则使用纯 reducer / 小型 hooks
- 关键流程使用 MSW 或模拟服务端进行集成测试
- DevTools:Redux/Query 面板——确保团队知道如何检查缓存
最终审查清单
- - [ ] 状态已分类:远程 vs UI vs URL vs 派生
- [ ] 每个数据源单一所有者;失效策略明确
- [ ] 为 API 数据选择了服务端缓存策略
- [ ] 异步/错误/空状态/乐观更新路径已指定
- [ ] 持久化/安全边界已遵守
有效指导技巧
- - 从最小化开始:Context + 服务端状态库可解决许多Redux 规模的问题。
- 始终问:变更返回后,数据源是什么?
- 调试奇怪的效果行为时,提及 React Strict Mode 的双重调用。
处理偏差
- - 遗留的全 Redux 架构:增量迁移——先处理功能模块,再将远程数据迁移到 Query。
- 非 React 环境:将模式映射到 signals/stores(Vue Pinia、Svelte stores),遵循相同的所有权原则。