NGXS 状態管理 + Facade パターン
NGXS とは
NGXS は、Angular 向けの状態管理ライブラリです。Redux パターンをシンプルに実装でき、TypeScript のデコレーターを活用した直感的な API を提供します。
Redux との比較
| 項目 | Redux (NgRx) | NGXS |
|---|---|---|
| ボイラープレート | 多い | 少ない |
| 学習コスト | 高い | 低い |
| TypeScript | 設定が必要 | ネイティブサポート |
| デコレーター | 使用しない | 活用 |
| テスタビリティ | 高い | 高い |
ディレクトリ構成
NGXS の状態管理は domains/ 配下に配置します。
domains/{domain}/
├── store/
│ ├── {domain}.state.ts # State 定義 + Selector
│ └── {domain}.actions.ts # Action 定義
├── {domain}.facade.ts # Store アクセスの抽象化
└── index.ts # エクスポート
State の定義
State クラスでは、状態のモデルと初期値、Selector、Action ハンドラーを定義します。
基本的な State
// domains/home/store/home.state.ts
import { Injectable } from '@angular/core';
import { Action, Selector, State, StateContext } from '@ngxs/store';
import { Increment } from './home.actions';
/** Form model for text input */
export interface TextFormModel {
text: string;
}
/** NGXS form plugin metadata */
export interface TextFormState {
model: TextFormModel;
dirty: boolean;
status: string;
errors: Record<string, unknown>;
}
export interface HomeStateModel {
count: number;
textForm: TextFormState;
}
@State<HomeStateModel>({
name: 'home',
defaults: {
count: 0,
textForm: {
model: { text: '' },
dirty: false,
status: '',
errors: {},
},
},
})
@Injectable()
export class HomeState {
@Selector()
static getCount(state: HomeStateModel): number {
return state.count;
}
@Selector()
static getTextFormModel(state: HomeStateModel): TextFormModel {
return state.textForm.model;
}
@Selector()
static getTextFormStatus(state: HomeStateModel): string {
return state.textForm.status;
}
@Selector()
static getTextFormDirty(state: HomeStateModel): boolean {
return state.textForm.dirty;
}
@Action(Increment)
increment(ctx: StateContext<HomeStateModel>): void {
const state = ctx.getState();
ctx.patchState({
count: state.count + 1,
});
}
}
ポイント解説
| デコレーター | 役割 |
|---|---|
@State |
状態の名前と初期値を定義 |
@Selector |
状態から値を取得する関数を定義 |
@Action |
Action がディスパッチされたときの処理を定義 |
Action の定義
Action は、状態変更のトリガーとなるクラスです。
// domains/home/store/home.actions.ts
export class Increment {
static readonly type = '[Home] Increment';
}
export class SetCount {
static readonly type = '[Home] Set Count';
constructor(public readonly count: number) {}
}
export class LoadData {
static readonly type = '[Home] Load Data';
}
Action の命名規則
// [ドメイン名] 操作名
static readonly type = '[Home] Increment';
static readonly type = '[User] Load Profile';
static readonly type = '[Cart] Add Item';
Facade パターン
Facade パターン を使用して、Store へのアクセスを抽象化します。これにより、コンポーネントと Store の結合度を下げられます。
Facade の実装
// domains/home/home.facade.ts
import { Injectable, inject } from '@angular/core';
import { Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { Increment } from './store/home.actions';
import { HomeState, type TextFormModel } from './store/home.state';
/**
* Facade service for Home domain
* Abstracts store access and provides a clean API for components
*/
@Injectable({
providedIn: 'root',
})
export class HomeFacade {
private readonly store = inject(Store);
/** Observable of the current count */
readonly count$: Observable<number> = this.store.select(HomeState.getCount);
/** Observable of the text form model */
readonly textFormModel$: Observable<TextFormModel> = this.store.select(HomeState.getTextFormModel);
/** Observable of the text form status (VALID, INVALID, etc.) */
readonly textFormStatus$: Observable<string> = this.store.select(HomeState.getTextFormStatus);
/** Observable of the text form dirty state */
readonly textFormDirty$: Observable<boolean> = this.store.select(HomeState.getTextFormDirty);
/** Increment the counter */
increment(): void {
this.store.dispatch(new Increment());
}
}
Facade の利点
| 利点 | 説明 |
|---|---|
| 疎結合 | コンポーネントが Store の詳細を知らなくてよい |
| テスタビリティ | Facade をモックするだけでテスト可能 |
| リファクタリング | Store の内部実装を変更しても影響が少ない |
| 型安全 | TypeScript の型推論が効く |
コンポーネントからの使用
// app/home/home.ts
import { Component, inject } from '@angular/core';
import { RxLet } from '@rx-angular/template/let';
import { HomeFacade } from '$domains/home';
import { ButtonDirective } from '$shared/ui/button';
@Component({
selector: 'app-home',
standalone: true,
imports: [RxLet, ButtonDirective],
template: `
<div *rxLet="count$; let count">
<p>Count: {{ count }}</p>
<button appButton (click)="increment()">+1</button>
</div>
`,
})
export class HomeComponent {
private readonly facade = inject(HomeFacade);
readonly count$ = this.facade.count$;
increment(): void {
this.facade.increment();
}
}
フォーム連携(@ngxs/form-plugin)
@ngxs/form-plugin を使用すると、Reactive Forms と NGXS Store を自動同期できます。
設定
// app.config.ts
import { withNgxsFormPlugin } from '@ngxs/form-plugin';
import { provideStore } from '@ngxs/store';
export const appConfig: ApplicationConfig = {
providers: [
provideStore(APP_STATES, withNgxsFormPlugin()),
],
};
State にフォーム状態を追加
// 状態モデル
export interface HomeStateModel {
count: number;
textForm: TextFormState; // フォーム状態
}
// 初期値
@State<HomeStateModel>({
name: 'home',
defaults: {
count: 0,
textForm: {
model: { text: '' },
dirty: false,
status: '',
errors: {},
},
},
})
テンプレートでの使用
<!-- ngxsForm でフォームと Store を紐付け -->
<form [formGroup]="textForm" ngxsForm="home.textForm">
<app-input-field
label="テキスト"
[control]="textForm.controls.text"
>
<input appInput formControlName="text" />
</app-input-field>
</form>
フォーム状態の自動同期
ngxsForm="home.textForm" を指定すると、以下が自動的に同期されます:
| プロパティ | 説明 |
|---|---|
model |
フォームの値 |
dirty |
値が変更されたか |
status |
バリデーション状態(VALID 等) |
errors |
バリデーションエラー |
Store の登録
State を app.config.ts で登録します。
// domains/index.ts
import { HomeState } from './home/store/home.state';
export const APP_STATES = [HomeState];
// app.config.ts
import { APP_STATES } from '$domains';
export const appConfig: ApplicationConfig = {
providers: [
provideStore(APP_STATES, withNgxsFormPlugin()),
],
};
DevTools との連携
開発時には Redux DevTools で状態を確認できます。
import { withNgxsReduxDevtoolsPlugin } from '@ngxs/devtools-plugin';
export const appConfig: ApplicationConfig = {
providers: [
provideStore(
APP_STATES,
withNgxsFormPlugin(),
withNgxsReduxDevtoolsPlugin({ disabled: !isDevMode() })
),
],
};
まとめ
NGXS + Facade パターンにより、以下を実現できます。
- シンプルな状態管理 - デコレーターベースの直感的な API
- Facade による疎結合 - コンポーネントと Store の分離
- フォーム連携 - Reactive Forms と Store の自動同期
- 型安全 - TypeScript のフルサポート
次のページでは、shadcn/ui スタイルの UI コンポーネント構成と Storybook について解説します。