SignalStore vs NgRx:企业项目状态管理该怎么选,不要盲目上大库
Angular 生态里,状态管理曾经是个"站队问题":要么裸着写,要么上 NgRx。现在有了 Signals ,尤其是 @ngrx/signals(SignalStore),天平彻底变了。
很多团队还在无脑复制老模板:新项目先装 @ngrx/store、@ngrx/effects、@ngrx/entity......结果一个中后台表单页,状态代码比业务代码还多。这篇文章帮你把"该不该上 NgRx"这件事,从玄学拉回工程决策。
一、先给结论:90% 的企业项目不需要"完整 NgRx"
如果你的项目符合以下大部分特征,不要上 NgRx:
- 中后台 CRUD、表单流转、列表/详情页为主
- 全局状态集中在:用户信息、权限、字典、几个共享列表
- 状态变更逻辑不复杂,没有大量"竞态、回滚、重试、离线同步"
- 团队规模 ≤ 10 人,或前端流动率较高
- 你希望新同事一周内能独立改业务代码
这类项目,SignalStore(或手写 Signal Service)是更优解。
NgRx 仍然有它的主场,但那更像是"少数关键系统",而不是"所有 Angular 项目的默认起点"。
二、两种方案的定位差异
| 维度 | SignalStore(@ngrx/signals / 手写 Signal Service) | NgRx(Store + Effects + Entities) |
|---|---|---|
| 心智模型 | 面向对象 + 响应式变量 | 函数式 + 事件溯源 |
| 学习曲线 | 低:signal / computed / effect | 高:Action / Reducer / Effect / Selector |
| 样板代码 | 极少 | 多(action / reducer / effect 文件) |
| 调试工具 | 基础(signal 值可见) | 强(Redux DevTools,时间旅行) |
| 性能 | 细粒度响应,极好 | 依赖 OnPush + 选择器记忆化 |
| 适合规模 | 中小型应用 / 局部复杂状态 | 大型、高合规、强审计系统 |
| 典型场景 | 中后台、SaaS 管理端 | 金融交易、工单流、复杂多步骤流程 |
一句话概括:
SignalStore 是"状态即变量",NgRx 是"状态即事件日志"。
三、SignalStore:企业项目的主流答案
1. 手写 Signal Service:简单直接,足够用
很多项目连 @ngrx/signals 都不用装,一个 Injectable + signal 就够:
typescript
@Injectable({ providedIn: 'root' })
export class OrdersStore {
// 源状态
private state = signal<{
orders: Order[];
filter: 'all' | 'pending' | 'paid';
loading: boolean;
}>({
orders: [],
filter: 'all',
loading: false,
});
// 对外只读投影
readonly orders = computed(() => this.state().orders);
readonly filter = computed(() => this.state().filter);
readonly loading = computed(() => this.state().loading);
readonly filteredOrders = computed(() => {
const { orders, filter } = this.state();
if (filter === 'all') return orders;
return orders.filter(o => o.status === filter);
});
// 命名 mutation
setFilter(filter: typeof this.state().filter) {
this.state.update(s => ({ ...s, filter }));
}
loadOrders() {
this.state.update(s => ({ ...s, loading: true }));
this.http.get<Order[]>('/api/orders').subscribe({
next: orders => this.state.update(s => ({ ...s, orders, loading: false })),
error: () => this.state.update(s => ({ ...s, loading: false })),
});
}
constructor(private http: HttpClient) {}
}
优点:
- 没有新概念,只有 Angular 原生 API
- 状态结构一目了然,新人一眼看懂
- 改状态就是
state.update(),比 dispatch 直观 - 和 zoneless + Signals 天然契合
缺点:
- 没有"强制规范",写烂了也会变成一团浆糊
- 没有 Redux DevTools(但企业项目真用到的比例不高)
经验值:10 个以内的"store 类"服务,手写完全 OK;超过这个数,再考虑 @ngrx/signals。
2. @ngrx/signals:有规范的 SignalStore
NgRx 团队很清醒,他们知道 Signals 是未来,所以做了 @ngrx/signals------用 Signals 实现"有结构的 store"。
javascript
import { signalStore, withState, withMethods, withComputed } from '@ngrx/signals';
import { computed, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
export const OrdersStore = signalStore(
{ providedIn: 'root' },
withState({
orders: [] as Order[],
filter: 'all' as const,
loading: false,
}),
withComputed(({ orders, filter }) => ({
filteredOrders: computed(() => {
if (filter() === 'all') return orders();
return orders().filter(o => o.status === filter());
}),
})),
withMethods((store, http = inject(HttpClient)) => ({
setFilter(filter: 'all' | 'pending' | 'paid') {
store.patchState({ filter });
},
loadOrders() {
store.patchState({ loading: true });
http.get<Order[]>('/api/orders').subscribe(orders => {
store.patchState({ orders, loading: false });
});
},
})),
);
为什么选它而不是手写?
- 统一团队写法(state / computed / methods 结构固定)
- 可组合:
withState / withMethods / withComputed / withHooks - 可以和 NgRx 生态其他部分(DevTools、Entity、Router Store)渐进集成
- 比手写更像"框架",比传统 NgRx 轻得多
适合场景:
- 中大型项目,需要统一状态规范
- 团队已经熟悉 NgRx 体系,但不想写那么多样板
- 未来可能演进到更复杂状态管理,但不想一步到位
四、NgRx 仍然不可替代的场景
下面这些特征,只要命中 2~3 条,NgRx 就是合理选择:
1. 强审计 / 可回放需求
- 金融交易系统
- 工单流转、审批流
- 需要"操作日志 + 回放 + 撤销"的业务
NgRx 的 Action → Reducer → Store 本质是事件溯源,天然支持:
- Redux DevTools 时间旅行
- 操作审计(谁在什么时间做了什么)
- 状态快照对比
SignalStore 也能记录日志,但那是"你自己维护的日志",不是"框架级事件流"。
2. 极度复杂的状态流转
例如:
- 多步骤向导,每一步依赖前面 N 步的结果
- 复杂表单,字段联动规则超过 50 条
- 实时协作(多人同时编辑同一份数据)
NgRx 的 Effects 专门处理"副作用编排":
- 请求重试、轮询
- 请求取消(race / takeUntil)
- 多请求合并、顺序依赖
虽然 Signals + RxJS 也能写,但 NgRx Effects 把这套逻辑"标准化"了,新人更容易看懂"数据流图"。
3. 大型团队 + 强代码规范
NgRx 的强约束是双刃剑:
- 好处:新人不能随便
setState,必须走 action → reducer,代码风格高度统一 - 坏处:样板代码多,写起来累
在 20+ 前端、跨团队共享 store 的项目里,这种"约束"反而是优点。
五、决策树:5 分钟判断你该用哪个
bash
是否需要时间旅行 / 操作审计?
├─ 是 → NgRx
└─ 否
├─ 项目规模小 / 中?
│ ├─ 是 → 手写 Signal Service
│ └─ 否
│ ├─ 团队已熟悉 NgRx?
│ │ ├─ 是 → @ngrx/signals
│ │ └─ 否 → 手写 Signal Service(优先)
└─ 状态逻辑是否极度复杂(多步流程 / 复杂副作用)?
├─ 是 → NgRx
└─ 否 → @ngrx/signals 或手写 Signal Service
一个现实经验:
大多数企业项目,走到最深的节点是「手写 Signal Service」,少部分走到「@ngrx/signals」,真正需要「完整 NgRx」的是少数关键系统。
六、迁移路径:别一次性重写
如果你已经在用 NgRx,不要为了 Signals 全盘推翻。
推荐渐进路线
-
新功能用 SignalStore
- 新模块、新页面,直接
signalStore() - 老 store 不动,避免大规模回归
- 新模块、新页面,直接
-
老 store 的 selector 暴露为 computed
scssreadonly filteredOrders = computed(() => this.store.selectSignal(selectFilteredOrders)()); -
Component Store → SignalStore
@ngrx/component-store是很好的过渡形态- 它本身就是"类 Signal"的思想,迁移成本很低
-
最后才考虑全局 Store 替换
- 只有当你发现维护成本 > 迁移成本时,才动全局 store
- 很多项目会长期"NgRx + SignalStore 混跑"
七、一个真实对比:同一个需求两种写法
需求:订单列表 + 筛选 + 加载态
NgRx 版(简化)
loadOrders actionloadOrdersSuccess actionsetFilter actionordersReducerordersSelectorsordersEffects- 组件
dispatch+select
文件数:6+,代码行数:200+
SignalStore 版
- 一个
OrdersStore loadOrders()方法setFilter()方法filteredOrderscomputed
文件数:1,代码行数:60
在 CRUD 业务里,SignalStore 的生产力优势是数量级的。
八、总结:别让"最佳实践"变成"过度工程"
Angular 社区过去十年有个惯性:
"状态管理 = NgRx"
现在 Signals 时代,这个等号不再成立。
- SignalStore(手写或 @ngrx/signals) 是企业项目的默认选择
- NgRx 是"有特定需求的复杂系统"的选项,而不是起点
- 盲目上大库,只会换来:更多样板、更慢迭代、更高的新人培训成本
一个健康的 Angular 项目,状态管理应该是这样的:
- 80% 的 CRUD 状态:SignalStore
- 15% 的组件内复杂状态:
signal + computed - 5% 的关键核心流程:NgRx
记住一句话:状态管理的目标是"可维护",不是"看起来专业"。