跳转到内容

State Management,还是不要 State Management?

现代前端应用已经很难真正“没有 State Management”。

这是一个很强的说法。绝对、挑衅,而且当然过于笼统。静态 Landing Page 不需要 Redux,只有三个字段的表单也不需要。谁要是为每次 Button Click 都引入一个 Global Store,大概率不是在解决架构问题,而是在制造新的问题。

但作为切入点,这个命题很有用,因为它能把真正需要讨论的问题暴露出来。

关键问题并不是:

我们需不需要 State Management?

而是:

我们的状态住在哪里,谁可以改变它,它又以多清晰的方式流经应用?

因为状态始终存在。区别只是:我们有意识地建模它,还是任由它偶然分散在 Component、Service、Subscription、Input/Output 链、Router Parameter 和局部变量里。

State 始终存在。

State Management 并不自动等于 Redux、NgRx、Signals、Store 或某个库。

它首先只意味着一件事:

应用的状态被有意识地管理。

这会带来一组很实际的问题:

  • 应用当前知道哪些数据?
  • 这些数据存在哪里?
  • 谁可以修改它们?
  • 修改通过什么触发?
  • 应用其他部分如何对此响应?
  • 如何从 Raw Data 得到 UI Model?
  • Loading、Error、Success 与 Side Effect 如何处理?

即使没有“显式 State Management”,应用依然有状态。

只是它经常散落在各处。

一点在 Component。 一点在 Service。 一点在 Router。 一点在某个 Subject。 一点在 Subscription。 一点在 Template。 再加一点希望:最好没人按错顺序操作。

这当然也是 State Management。

只是它是隐式的。

隐式 State Management。

State Management 解决的到底是什么问题

Section titled “State Management 解决的到底是什么问题”

State Management 并不会让复杂应用突然不复杂。

它解决的是另一个问题:

它让复杂度变得可见、可定位、可测试。

很多前端应用里,真正困难的甚至不是业务本身。业务流程可能非常简单:

  • 加载数据
  • 打开表单
  • 保存修改
  • 显示成功
  • 刷新列表
  • 导航到详情页
  • 显示错误

问题在于,这些步骤经常横跨整个应用。

Component 调 Service。 Service 里有一个 Subject。 另一个 Component 对它做 Subscription。 Toast 顺手在旁边触发。 Routing 又发生在 Callback 里。 Reload 挂在另一个 Callback 上。

最后没人说得清:某个错误到底来自 HTTP Call、Mapping、Template,还是一条忘记清理的旧 Subscription。

State Management 可以把这种散乱重新组织起来。

不是因为 Store 会魔法般生成好代码,而是因为它迫使团队思考“流”。

一个好的 State Management 方案至少会区分:

  • Intent:用户或系统想做什么?
  • Command / Event:什么被发送进应用?
  • Operation:发生了什么外部操作,例如 HTTP?
  • Result:得到什么业务结果?
  • State Update:状态如何变化?
  • ViewModel:UI 最终看到什么?
  • Side Effect:Toast、Navigation、Reload、Tracking 等

于是流程变得可追踪:

Button click
-> createIntent(payload)
-> event.on(createIntent)
-> http.put(payload)
-> createSuccess(result) oder createError(error)
-> unabhängige Reaktionen

响应式 Create Flow:UI 发送 Intent,Store 响应,Infrastructure 返回结果,Success / Error 再触发彼此独立的反应。

Button 并不负责启动整个业务流程。

它只表达一个意图。

UI 说:

我想创建这个东西。

但它并不决定接下来调用什么 Infrastructure、重新加载哪些数据、显示哪个 Toast 或导航到哪里。

这些反应属于应用流程,而不是 Button Click Handler。

这正是 State Management 带来的架构价值。

一个常见错误,是把 Success 后的所有动作做成线性的调用链。

例如:

save()
-> http.put()
-> reload()
-> showToast()
-> navigate()

这当然能工作。

但它把本来可以独立的行为强耦合在一起。

很多情况下,更好的表达是:

createSuccess(result)
├─ event.on(createSuccess) -> reload resource
├─ event.on(createSuccess) -> show toast
└─ event.on(createSuccess) -> navigate

看起来只是小改动,架构含义却不同。

第一种形式里,所有动作依附于一条命令式链。

第二种形式里,先出现一个具有业务含义的结果,多个应用部分可以分别对此响应。

Callback Chain 与独立 Event Reaction。

这样扩展也更容易。

以后如果还要发送 Analytics Event,不需要把现有 Save Flow 再拆开重组,只需要增加一个对 createSuccess 的新反应。

“什么时候需要 State Management?”

Section titled ““什么时候需要 State Management?””

这个问题很常见,也完全合理。

务实的回答是:

当一个状态被不止一个地方读取或修改时,显式 State Management 很快就开始值得。

常见信号包括:

  • 多个 Component 需要同一份数据
  • 某个 Action 之后需要一致地刷新数据
  • Loading / Error / Success State 不断重复
  • Save 后需要同时发生 Toast、Reload、Navigation
  • Component 越来越大
  • Service 同时承载业务逻辑、UI Logic 与 Cache Logic
  • 手工 Subscription 越来越多
  • Debugging 变成“这个值到底在哪里被 set 了?”
  • 因为逻辑挂在 Component 上,测试越来越困难

如果目标是训练自己真正理解响应式编程,答案甚至可以更严格:

如果你想学习响应式编程,就始终练习显式 State Management。

不是因为每个应用都必须这样做,而是因为它会训练一种更干净的思维方式:

  • 数据单向流动
  • 修改通过显式 Event 发生
  • UI 是 State 的派生
  • Side Effect 被有意识地建模
  • Component 是消费者,不是控制中心

开始时结构会多一点。

以后混乱会少很多。

什么时候哪种 State?

我曾经在一次培训里从一个很简单的问题开始:

你们希望这个项目采用响应式编程吗?

如果答案是“不”,那我们甚至不用讨论 Redux、Store 或 State Management。继续使用命令式方式也可以,只需要同时接受它带来的后果。

但如果答案是“是”,就必须认真讨论 State Management。

因为响应式编程并不等于代码里出现了一个 Observablesignal()

只有当 UI、数据和事件被有意识地建模成 Flow,应用才真正开始变得响应式。

为了说明这一点,我曾经把同一个 CRUD Flow 分别实现成三种方式:

  1. Observer Service
  2. 经典 Redux Pattern 的 NgRx
  3. NgRx Signals

业务完全相同:

  • 创建数据
  • 执行 HTTP Request
  • 处理成功或错误
  • 刷新列表
  • 显示 Success Toast
  • 程序化 Navigation
  • UI 与 Infrastructure / Application Logic 清晰分离

三种方案里的架构边界都刻意保持一致:

  • Infrastructure 负责 HTTP
  • Application Layer 负责 Use Case
  • State Layer 负责状态与 Flow
  • UI 只负责消费

有意思的并不是哪个库“赢了”。

真正有意思的是参与者注意到了一件事:

业务没有变化,但代码变少了。

这并不意味着 State Management 永远会减少代码。经典 Redux 完全可能产生大量 Boilerplate。

真正的规律是:工具越贴合所采用的 Paradigm,需要自己发明的机制就越少。

Observer Service、Redux Store、Signal Store:业务步骤相同,但 Boilerplate 与结构成本不同。

Observer Service:可以工作,但很多规则需要自己造

Section titled “Observer Service:可以工作,但很多规则需要自己造”

Observer Service 在小到中型场景里完全可以成立。

常见做法是:一个 Service,里面有 SubjectBehaviorSubject,再加一些 Action Method 和供 UI 消费的公开 Stream。

这并没有错。

问题在于:你实际上在自己实现一套小型 State Management。

于是也必须自己定义规则:

  • Event 怎么命名?
  • Error Handling 在哪里发生?
  • Loading 在哪里建模?
  • 谁更新 Cache?
  • 谁触发 Reload?
  • 谁允许调用 next()
  • Side Effect 放在哪里?
  • 怎样一致地测试?

高度自律的团队可以把它做好。

但长期来看,纪律通常没有结构那么容易扩展。

经典 NgRx:结构很强,仪式也很多

Section titled “经典 NgRx:结构很强,仪式也很多”

Redux 风格的 NgRx 提供了非常明确的规则:

  • Action 描述 Event
  • Reducer 改变 State
  • Effect 处理 Side Effect
  • Selector 为 UI 准备数据

从架构角度看,这很强,尤其适合较大的应用。

代价则是 Ceremony。

更多文件、更多类型、更多连接代码。对理解 Redux 的团队,这是显式结构;对只想“赶快加载一下数据”的团队,它会显得很重。

只有当应用复杂度足够高,这种结构节省的成本超过它自身带来的成本时,收益才真正显现。

NgRx Signals:代码更少,但基本思想没变

Section titled “NgRx Signals:代码更少,但基本思想没变”

NgRx Signals 并没有改变 State Management 的核心思想。

它只是让这种思想更贴近 Angular。

State、Computed Value、Method 和 Reactive Derivation 可以更紧凑地放在一起。过去分散在 Action、Reducer、Effect 和 Selector 里的某些结构,可以更靠近业务模型表达。

因此,同样一个 CRUD Flow 往往可以小很多。

不是因为架构消失了。

而是因为表达这套架构所需要的 Infrastructure Code 少了。

UI 仍然是消费者。 HTTP 仍然属于 Infrastructure。 业务 Operation 仍然需要清晰建模。 State 依然是显式的。

只是表达方式没有那么重。

State Management 不是免费的收益。

它会带来新的问题:

  • Local Component State 在哪里结束?
  • 什么应该进入 Store?
  • Store 应该切多细?
  • 如何避免 Global Store 变成垃圾场?
  • Event 与 Method 如何统一命名?
  • Command State 与 Read State 怎么分离?
  • 如何防止 Store 成为新的 God Class?

糟糕的 State Management 只是另一种糟糕的代码。

把所有东西放进一个 Global Store,不叫架构。

那只是地下室。

Anti-Pattern:Store 变成地下室。

State Management 只有与明确边界结合时才真正有帮助:

  • Feature Store,而不是 Global Data Dump
  • ViewModel,而不是 Template Logic
  • 明确 Event / Intent,而不是任意 Method
  • 有意识地建模 Side Effect
  • 保持 Component 小而清晰
  • Infrastructure 与 UI 分离
  • 使用 Derivation,而不是手工同步

“代码更少”并不是最重要的收益

Section titled ““代码更少”并不是最重要的收益”

代码更少当然很好。

但更重要的是:

你知道应该去哪里找问题。

Save 失败,我看 Command Flow。 数据展示错误,我看 ViewModel。 Toast 没出现,我看 Success Handler。 Reload 没发生,我看谁对 Success Event 做反应。 UI 行为异常,我首先看它是否意外拥有了自己的业务逻辑。

这会显著改善 Debugging。

也会改善测试。

如果 Component 只消费 State 并发送 Intent,就不需要在 UI 层测试太多逻辑。真正的逻辑会落在 Function、Store、Mapper 和 Use Case 中,更容易做快速、稳定的测试。

Debugging:我应该去哪里找?

“要不要 State Management?”通常太粗。

更好的问题是:

  • 我们的 State Flow 有多复杂?
  • 多少 Component 共享数据?
  • 一次 Action 多经常需要触发后续反应?
  • Debugging 的可追踪性有多重要?
  • Business Logic 需要多高的可测试性?
  • 团队对响应式编程理解到什么程度?
  • Component 变成控制中心的风险有多大?

在现代前端应用里,答案经常不再是:

我们不需要 State Management。

而更像是:

我们不需要在所有地方使用同一级别的 State Management。

局部 UI State 可以留在局部。

Dropdown 是否展开,不需要进入 Global Store。 Dialog 是否显示,通常也不需要。 Form Field 是否 Focus,更不需要。

但业务状态、已加载数据、Command Flow、权限、Selection、Filter、Loading State 与 Derived ViewModel,几乎总能从显式结构中获益。

“现代前端应用已经离不开 State Management。”

作为绝对命题,它当然不准确。

作为架构警告,它却非常接近事实。

因为状态始终存在。我们真正要决定的是:有意识地管理它,还是让它随着应用增长而自行扩散。

好的 State Management 不意味着:

我们用了某个库。

它意味着:

  • 清晰的数据流
  • 清晰的 Event
  • 清晰的职责
  • 小而简单的 UI Component
  • 更好的测试
  • 更好的 Debugging
  • 更少的隐式耦合
  • 更少偶然形成的架构

所以更好的问题不是:

到什么时候我们才需要 State Management?

而是:

到什么时候,我们还承担得起没有它的代价?