Web

NgRx学习笔记:基础篇

Posted by Kerwen Blog on September 23, 2025

在 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。

注意两点:

  1. Reducer 不做异步请求
  2. 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”按钮时,流程可以是:

  1. 组件先 dispatch 一个 Delay Increment Action
  2. Effect 监听到这个 Action
  3. 等待 2 秒
  4. Effect 再 dispatch 一个真正的 Increment Action
  5. 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
    }))
);

这里很好理解:

  • 收到 incrementcount + 1
  • 收到 decrementcount - 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
);

这里做了两件事:

  1. 先选中 counter 这一块状态
  2. 再从里面取出 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