NgRx は Angular エコシステムで最も主流の状態管理ライブラリで、Redux パターン + RxJS をベースとしている。中〜大規模な Angular アプリケーションにおいて、NgRx は状態の流れを予測可能・デバッグ可能・テスト可能にする。この記事では、NgRx のコアコンセプトと実際の使い方を一通り追ってみる。
コアコンセプト
Store(单一状态树)
↑ 读取 ↓ 分发
Component ——dispatch(Action)——→ Reducer(纯函数)→ 新 State
↑ select ↑
└──────── Effect(副作用:HTTP 请求等)
インストール
bash
ng add @ngrx/store @ngrx/effects @ngrx/entity @ngrx/store-devtools
State、Action、Reducer の定義
typescript
// users/users.state.ts
import { createReducer, createAction, on, props } from "@ngrx/store";
import { createEntityAdapter, EntityState } from "@ngrx/entity";
export interface User {
id: number;
name: string;
email: string;
}
// Actions
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 deleteUser = createAction(
"[Users] Delete User",
props<{ id: number }>(),
);
// Entity Adapter(自动管理 ids + entities 结构)
export const adapter = createEntityAdapter<User>();
export interface UsersState extends EntityState<User> {
loading: boolean;
error: string | null;
}
const initialState: UsersState = adapter.getInitialState({
loading: false,
error: null,
});
// Reducer
export const usersReducer = createReducer(
initialState,
on(loadUsers, (state) => ({ ...state, loading: true, error: null })),
on(loadUsersSuccess, (state, { users }) =>
adapter.setAll(users, { ...state, loading: false }),
),
on(loadUsersFailure, (state, { error }) => ({
...state,
loading: false,
error,
})),
on(deleteUser, (state, { id }) => adapter.removeOne(id, state)),
);
Selectors
typescript
// users/users.selectors.ts
import { createFeatureSelector, createSelector } from "@ngrx/store";
const selectUsersFeature = createFeatureSelector<UsersState>("users");
const { selectAll, selectEntities, selectTotal } = adapter.getSelectors();
export const selectAllUsers = createSelector(selectUsersFeature, selectAll);
export const selectUsersLoading = createSelector(
selectUsersFeature,
(s) => s.loading,
);
export const selectUsersError = createSelector(
selectUsersFeature,
(s) => s.error,
);
// 派生 selector
export const selectActiveUsers = createSelector(selectAllUsers, (users) =>
users.filter((u) => u.active),
);
Effects(处理副作用)
typescript
// users/users.effects.ts
import { Injectable } from "@angular/core";
import { Actions, createEffect, ofType } from "@ngrx/effects";
import { of } from "rxjs";
import { map, exhaustMap, catchError } from "rxjs/operators";
@Injectable()
export class UsersEffects {
loadUsers$ = createEffect(() =>
this.actions$.pipe(
ofType(loadUsers),
exhaustMap(() =>
this.userService.getUsers().pipe(
map((users) => loadUsersSuccess({ users })),
catchError((err) => of(loadUsersFailure({ error: err.message }))),
),
),
),
);
constructor(
private actions$: Actions,
private userService: UserService,
) {}
}
コンポーネントでの使用
typescript
@Component({
template: `
<div *ngIf="loading$ | async">加载中...</div>
<div *ngIf="error$ | async as error" class="error">{{ error }}</div>
<ul>
<li *ngFor="let user of users$ | async">
{{ user.name }}
<button (click)="onDelete(user.id)">删除</button>
</li>
</ul>
<button (click)="onLoad()">加载用户</button>
`,
})
export class UsersComponent {
users$ = this.store.select(selectAllUsers);
loading$ = this.store.select(selectUsersLoading);
error$ = this.store.select(selectUsersError);
constructor(private store: Store) {}
onLoad() {
this.store.dispatch(loadUsers());
}
onDelete(id: number) {
this.store.dispatch(deleteUser({ id }));
}
}
AppModule への登録
typescript
@NgModule({
imports: [
StoreModule.forRoot({ users: usersReducer }),
EffectsModule.forRoot([UsersEffects]),
StoreDevtoolsModule.instrument({
maxAge: 25,
logOnly: environment.production,
}),
],
})
export class AppModule {}
NgRx を選ぶべき時
適している場合:
- 互いに関係のない複数のコンポーネント間で状態を共有する場合
- タイムトラベルデバッグ(Redux DevTools)が必要な場合
- 複雑な非同期フロー(複数の API 依存、楽観的更新)がある場合
適さない場合:
- 小規模なアプリケーション(BehaviorSubject + Service で十分な場合)
- 単純な親子コンポーネント間の通信(@Input/@Output で済む場合)
まとめ
NgRx の学習曲線はやや急だが、一度 Action → Reducer → Store → Selector → Effect という一連の流れを理解すれば、状態ロジックのデバッグとテストがとても扱いやすくなる。NgRx DevTools のタイムトラベルデバッグは、エンタープライズ級アプリケーションでバグを探る際の強力な武器だ。
