在 Angular 项目里,组件一多、共享数据一多,状态管理就会慢慢变成一个绕不开的问题。
NgRx 是 Angular 生态里很常见的一套状态管理方案。第一次接触它时,最容易被一堆术语劝退:
- Store
- Actions
- Reducers
- Selectors
- Effects
其实先不用急着记语法,也不用一上来就看复杂例子。NgRx 的核心思路并不复杂:
用固定的流程管理状态变化,让数据流更清晰,也更容易追踪。
这篇先只整理基础部分,重点放在几个核心概念本身:它们分别做什么,彼此之间是什么关系。
先用最简单的话理解 NgRx
假设页面上有一个计数器,初始值是 0。
点击按钮后数字加一,或者两秒后再加一。
先说一个最容易混淆的概念:Store。
先理解 Store 是什么
可以把 Store 理解成应用里的一个统一状态仓库。
比如一个页面里有:
- 当前登录用户信息
- 购物车数量
- 是否加载中
- 列表筛选条件
- 某个模块的计数器数值
这类会变化、又可能被多个组件共享的数据,都可以放进 Store 里统一管理。
简单说,Store 负责存状态,NgRx 里的其他几个概念负责定义:状态怎么改、怎么读。
围绕 Store,NgRx 通常会拆成 4 个角色:
- Action:发生了什么事
- Reducer:状态怎么变化
- Selector:我要从状态里取什么数据
- Effect:处理异步任务或副作用
1. Action:描述“发生了什么”
Action 的作用很简单:
只描述发生了什么,不负责修改数据。
比如用户点击了“+1”按钮,我们可以发出一个 Action:
1
{ type: '[Counter] Increment' }
意思就是:
- 在 Counter 这个功能里
- 发生了 Increment 这件事
可以把 Action 理解成一条消息,或者一个事件。
它只负责通知系统:某件事发生了。
2. Reducer:决定“状态怎么变”
Reducer 负责接收当前状态和 Action,然后返回一个新的状态。
比如当前状态是:
1
{ count: 0 }
如果收到了 Increment 这个 Action,那么 Reducer 返回:
1
{ count: 1 }
Reducer 的职责就是:
根据 Action 计算出新的 State。
注意两点:
- Reducer 不做异步请求
- Reducer 不直接修改原对象,而是返回一个新对象
3. Selector:从状态里“取数据”
Store 里往往会放很多状态数据,组件没必要每次都自己去状态树里翻。
Selector 的作用就是:
从 Store 中把需要的数据选出来。
比如状态是:
1
2
3
4
5
{
counter: {
count: 3
}
}
Selector 可以帮我们直接取出:
1
3
组件只关心“我要显示什么”,至于这个值在状态树的哪一层,由 Selector 统一处理。
4. Effect:处理异步和副作用
有些逻辑不适合写在 Reducer 里,比如:
- 调用后端 API
- 延迟执行
- 读写 localStorage
- 路由跳转
- 打日志
这些都属于“副作用”,通常交给 Effect 处理。
例如点击“2 秒后 +1”按钮时,流程可以是:
- 组件先 dispatch 一个
Delay IncrementAction - Effect 监听到这个 Action
- 等待 2 秒
- Effect 再 dispatch 一个真正的
IncrementAction - Reducer 收到
Increment后修改状态
Effect 的职责就是:
监听 Action,执行异步任务或副作用,然后再派发新的 Action。
一张图理解 4 个核心概念
不考虑异步时,最基本的流程是:
1
2
3
4
5
6
7
8
9
组件点击按钮
↓
dispatch Action
↓
Reducer 根据 Action 更新 State
↓
Selector 从 State 中取数据
↓
组件显示最新数据
有异步时:
1
2
3
4
5
6
7
8
9
10
11
组件 dispatch Action
↓
Effect 监听 Action
↓
执行异步任务
↓
Effect dispatch 新的 Action
↓
Reducer 更新 State
↓
Selector 取数据给组件
可以把它们简单记成:
- Action:通知发生了什么
- Reducer:计算新状态
- Selector:读取状态
- Effect:处理异步
如果这四者的分工已经比较清楚了,下面再用一个最小的 Counter 例子,把整个流程串起来。
用一个最小 Counter 例子理解 NgRx
下面用一个最简单的计数器例子,把这四个概念串起来。
安装依赖
1
npm install @ngrx/store @ngrx/effects
1. 定义 Actions
先创建 counter.actions.ts:
1
2
3
4
5
import { createAction } from '@ngrx/store';
export const increment = createAction('[Counter] Increment');
export const decrement = createAction('[Counter] Decrement');
export const delayIncrement = createAction('[Counter] Delay Increment');
这里定义了 3 个 action:
increment:立刻加一decrement:立刻减一delayIncrement:延迟后加一
这里先不用纠结语法,重点是理解:
Action 只是描述“发生了什么事”。
2. 定义 Reducer
创建 counter.reducer.ts:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { createReducer, on } from '@ngrx/store';
import { increment, decrement } from './counter.actions';
export interface CounterState {
count: number;
}
export const initialState: CounterState = {
count: 0
};
export const counterReducer = createReducer(
initialState,
on(increment, (state) => ({
...state,
count: state.count + 1
})),
on(decrement, (state) => ({
...state,
count: state.count - 1
}))
);
这里很好理解:
- 收到
increment,count + 1 - 收到
decrement,count - 1
Reducer 不关心这个 Action 是谁发出来的,它只关心:
如果发生了这个 Action,State 应该变成什么样。
3. 注册 Store
在 app.module.ts 中注册 reducer:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import { AppComponent } from './app.component';
import { counterReducer } from './counter.reducer';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
StoreModule.forRoot({ counter: counterReducer })
],
bootstrap: [AppComponent],
})
export class AppModule {}
这里的:
1
StoreModule.forRoot({ counter: counterReducer })
表示全局状态里有一个 counter 字段,这一块状态由 counterReducer 管理。
也就是说,Store 大概会长这样:
1
2
3
4
5
{
counter: {
count: 0
}
}
4. 定义 Selector
创建 counter.selectors.ts:
1
2
3
4
5
6
7
8
9
10
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { CounterState } from './counter.reducer';
export const selectCounterState =
createFeatureSelector<CounterState>('counter');
export const selectCount = createSelector(
selectCounterState,
(state) => state.count
);
这里做了两件事:
- 先选中
counter这一块状态 - 再从里面取出
count
也就是说:
selectCounterState取到的是{ count: 0 }selectCount取到的是0
Selector 的意义就在这里:
组件不用关心状态结构细节,只需要拿结果。
5. 定义 Effect
创建 counter.effects.ts:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { map, delay } from 'rxjs/operators';
import { delayIncrement, increment } from './counter.actions';
@Injectable()
export class CounterEffects {
delayIncrement$ = createEffect(() =>
this.actions$.pipe(
ofType(delayIncrement),
delay(2000),
map(() => increment())
)
);
constructor(private actions$: Actions) {}
}
它的作用就是:
- 监听
delayIncrement - 等 2 秒
- 再发出一个
increment
注意:
Effect 本身不直接修改状态。
真正修改状态的仍然是 Reducer。
6. 注册 Effects
还是在 app.module.ts 中注册:
1
2
3
4
5
6
7
8
9
10
11
12
13
import { EffectsModule } from '@ngrx/effects';
import { CounterEffects } from './counter.effects';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
StoreModule.forRoot({ counter: counterReducer }),
EffectsModule.forRoot([CounterEffects])
],
bootstrap: [AppComponent],
})
export class AppModule {}
7. 在组件里使用 Store
修改 app.component.ts:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { increment, decrement, delayIncrement } from './counter.actions';
import { selectCount } from './counter.selectors';
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {
count$: Observable<number>;
constructor(private store: Store) {
this.count$ = this.store.select(selectCount);
}
increment() {
this.store.dispatch(increment());
}
decrement() {
this.store.dispatch(decrement());
}
delayIncrement() {
this.store.dispatch(delayIncrement());
}
}
这里有两个点:
读取数据
1
this.count$ = this.store.select(selectCount);
通过 Selector 从 Store 里读取 count。
修改数据
1
this.store.dispatch(increment());
通过 dispatch Action 触发状态变化。
8. 模板显示
修改 app.component.html:
1
2
3
4
<button (click)="increment()">+1</button>
<span>8</span>
<button (click)="decrement()">-1</button>
<button (click)="delayIncrement()">2秒后+1</button>
运行之后:
- 点击
+1,数字立刻加一 - 点击
-1,数字立刻减一 - 点击
2秒后+1,两秒后数字加一
这 4 个概念到底有什么区别?
Action
描述“发生了什么”。
例如:
1
increment()
它本质上只是一个事件通知。
Reducer
根据 Action 计算新状态。
例如:
1
2
3
4
on(increment, state => ({
...state,
count: state.count + 1
}))
它负责真正更新 Store 里的数据。
Selector
从 Store 中取数据。
例如:
1
2
3
4
export const selectCount = createSelector(
selectCounterState,
state => state.count
);
它负责把组件需要的数据取出来。
Effect
处理异步任务或副作用。
例如:
1
2
3
ofType(delayIncrement),
delay(2000),
map(() => increment())
它负责监听 Action,处理异步逻辑,再发出新的 Action。
为什么要把它们拆开?
拆开之后,每个角色的职责都很清楚:
- 组件:负责触发动作、显示数据
- Action:负责描述发生了什么
- Reducer:负责计算新状态
- Selector:负责取数据
- Effect:负责异步逻辑
这样做的好处是:
- 数据流清晰
- 逻辑更容易维护
- 更容易测试
- 状态变化更可预测
什么时候适合使用 NgRx?
不是所有 Angular 项目都必须上 NgRx。
如果项目很小、状态也不复杂,直接用 service 管理状态通常就够了。
更适合使用 NgRx 的场景一般是:
- 多个组件需要共享同一份状态
- 页面逻辑复杂,状态变化比较多
- 有较多异步操作
- 希望状态变化可追踪、可预测
- 中大型项目需要统一状态管理方式
反过来,如果只是简单页面、简单交互,状态也不共享,那直接引入 NgRx 反而会增加理解和维护成本。
基础篇小结
基础篇先到这里。
如果只记一句话,那就是:
组件通过 dispatch 发出 Action,Reducer 负责更新状态,Selector 负责读取状态,Effect 负责处理异步流程。
把这条主线理顺之后,再去看 feature state、createAction 的 props、Effects 里调用 API、Entity、Router Store 这些内容,就不会那么乱了。
下一篇再继续整理 NgRx 的进阶内容,比如:
- 异步请求的完整处理流程
- success / failure action 的拆分方式
- feature module 中的状态拆分
- selector 组合与复用
- 项目里更常见的目录组织方式
Reference
NgRx
Angular与NgRx状态管理: 最佳实践解析
Level Up Your NgRx Skills With 10 Time-Tested Best Practices
NgRx — Best Practices for Enterprise Angular Applications
Simplify State Management with NgRx in Angular | NgRx Guide
Angular Ngrx Undo Redo Demo