NgRx 完整状态管理流程实战
更新: 8/13/2026字数: 0 字 时长: 0 分钟
本文通过一个完整的「用户管理」案例,从零到一演示 NgRx 状态管理的完整流程:定义状态 → Action → Reducer → Effect → Selector → 组件使用。
一、项目结构约定
一个标准的 NgRx 特性目录结构如下:
src/app/features/users/
├── store/
│ ├── users.actions.ts # Action 定义
│ ├── users.reducer.ts # Reducer
│ ├── users.effects.ts # Effect(副作用)
│ ├── users.selectors.ts # Selector
│ └── users.state.ts # 状态模型(可选,单独抽离)
├── users.component.ts # 组件
└── users.service.ts # 数据服务二、定义状态模型
typescript
// users.state.ts
export interface User {
id: number;
name: string;
email: string;
}
export interface UsersState {
users: User[];
selectedUserId: number | null;
loading: boolean;
error: string | null;
}
export const initialState: UsersState = {
users: [],
selectedUserId: null,
loading: false,
error: null
};三、定义 Action
typescript
// users.actions.ts
import { createAction, props } from "@ngrx/store";
import { User } from "./users.state";
// 加载
export const loadUsers = createAction("[Users] Load Users");
export const loadUsersSuccess = createAction("[Users] Load Users Success", props<{ users: User[] }>());
export const loadUsersFailure = createAction("[Users] Load Users Failure", props<{ error: string }>());
// 选择
export const selectUser = createAction("[Users] Select User", props<{ id: number }>());
// 新增
export const addUser = createAction("[Users] Add User", props<{ user: User }>());
export const addUserSuccess = createAction("[Users] Add User Success", props<{ user: User }>());
// 删除
export const deleteUser = createAction("[Users] Delete User", props<{ id: number }>());四、编写 Reducer
typescript
// users.reducer.ts
import { createReducer, on } from "@ngrx/store";
import * as UsersActions from "./users.actions";
import { UsersState, initialState } from "./users.state";
export const usersReducer = createReducer(
initialState,
on(UsersActions.loadUsers, (state) => ({
...state,
loading: true,
error: null
})),
on(UsersActions.loadUsersSuccess, (state, { users }) => ({
...state,
users,
loading: false
})),
on(UsersActions.loadUsersFailure, (state, { error }) => ({
...state,
loading: false,
error
})),
on(UsersActions.selectUser, (state, { id }) => ({
...state,
selectedUserId: id
})),
on(UsersActions.addUserSuccess, (state, { user }) => ({
...state,
users: [...state.users, user]
})),
on(UsersActions.deleteUser, (state, { id }) => ({
...state,
users: state.users.filter((u) => u.id !== id)
}))
);五、编写 Effect
typescript
// users.effects.ts
import { Injectable } from "@angular/core";
import { Actions, createEffect, ofType } from "@ngrx/effects";
import { of } from "rxjs";
import { map, mergeMap, catchError } from "rxjs/operators";
import * as UsersActions from "./users.actions";
import { UsersService } from "../users.service";
@Injectable()
export class UsersEffects {
// 加载用户列表
loadUsers$ = createEffect(() =>
this.actions$.pipe(
ofType(UsersActions.loadUsers),
mergeMap(() =>
this.usersService.getUsers().pipe(
map((users) => UsersActions.loadUsersSuccess({ users })),
catchError((error) => of(UsersActions.loadUsersFailure({ error: error.message })))
)
)
)
);
// 新增用户
addUser$ = createEffect(() =>
this.actions$.pipe(
ofType(UsersActions.addUser),
mergeMap(({ user }) =>
this.usersService.addUser(user).pipe(
map((created) => UsersActions.addUserSuccess({ user: created })),
catchError((error) => of(UsersActions.loadUsersFailure({ error: error.message })))
)
)
)
);
constructor(private actions$: Actions, private usersService: UsersService) {}
}六、编写 Selector
typescript
// users.selectors.ts
import { createFeatureSelector, createSelector } from "@ngrx/store";
import { UsersState } from "./users.state";
export const selectUsersState = createFeatureSelector<UsersState>("users");
export const selectAllUsers = createSelector(selectUsersState, (state) => state.users);
export const selectLoading = createSelector(selectUsersState, (state) => state.loading);
export const selectError = createSelector(selectUsersState, (state) => state.error);
export const selectSelectedUserId = createSelector(selectUsersState, (state) => state.selectedUserId);
// 组合选择器:根据选中的 id 获取具体用户
export const selectSelectedUser = createSelector(selectAllUsers, selectSelectedUserId, (users, id) =>
users.find((u) => u.id === id) || null
);七、注册模块
typescript
// app.module.ts(或独立组件应用的 providers)
import { StoreModule } from "@ngrx/store";
import { EffectsModule } from "@ngrx/effects";
import { usersReducer } from "./users/store/users.reducer";
import { UsersEffects } from "./users/store/users.effects";
@NgModule({
imports: [
StoreModule.forRoot({ users: usersReducer }),
EffectsModule.forRoot([UsersEffects])
]
})
export class AppModule {}独立组件应用(app.config.ts):
typescript
import { provideStore } from "@ngrx/store";
import { provideEffects } from "@ngrx/effects";
import { usersReducer } from "./users/store/users.reducer";
import { UsersEffects } from "./users/store/users.effects";
export const appConfig: ApplicationConfig = {
providers: [
provideStore({ users: usersReducer }),
provideEffects([UsersEffects])
]
};八、组件中使用
typescript
// users.component.ts
import { Component, OnInit } from "@angular/core";
import { Store } from "@ngrx/store";
import { Observable } from "rxjs";
import { User } from "./store/users.state";
import * as UsersActions from "./store/users.actions";
import { selectAllUsers, selectLoading, selectError, selectSelectedUser } from "./store/users.selectors";
@Component({
selector: "app-users",
template: `
<h2>用户管理</h2>
<div *ngIf="loading$ | async">加载中...</div>
<div *ngIf="error$ | async" style="color: red">{{ error$ | async }}</div>
<ul>
<li *ngFor="let user of users$ | async" (click)="onSelect(user.id)">
{{ user.name }} - {{ user.email }}
</li>
</ul>
<div *ngIf="selectedUser$ | async as selected">
<p>当前选中:{{ selected.name }}</p>
</div>
`
})
export class UsersComponent implements OnInit {
users$: Observable<User[]> = this.store.select(selectAllUsers);
loading$ = this.store.select(selectLoading);
error$ = this.store.select(selectError);
selectedUser$ = this.store.select(selectSelectedUser);
constructor(private store: Store) {}
ngOnInit() {
this.store.dispatch(UsersActions.loadUsers());
}
onSelect(id: number) {
this.store.dispatch(UsersActions.selectUser({ id }));
}
}九、完整流程回顾
- 组件 dispatch
loadUsersAction - Effect 监听到
loadUsers,调用服务发请求 - 请求成功后 Effect dispatch
loadUsersSuccess - Reducer 处理
loadUsersSuccess,更新 Store 状态 - Selector 从 Store 提取数据
- 组件 通过
select订阅到最新数据并渲染
十、常见问题解答
Q1:为什么加载要拆成 loadUsers / loadUsersSuccess / loadUsersFailure 三个 Action?
- 这是
NgRx的推荐模式,把「开始请求」「成功」「失败」三种状态分离,让状态机清晰、便于在 Reducer 中精确处理每种情况。
Q2:State、Action、Reducer 等文件可以合并吗?
- 可以,但官方推荐按职责拆分文件,遵循「每个特性一个 store 目录」的约定,便于维护和团队协作。
Q3:Effects 里的 mergeMap 能换成 switchMap 吗?
- 视场景而定:需要取消旧请求(如搜索)用
switchMap,需要并行/不取消(如列表加载)用mergeMap。