フロントエンドアプリケーションがますます複雑になり、複数のアクターがさまざまな方法でアプリケーションのグローバル状態に影響を与える可能性があるため、状態で何が起こっているのかを制御できなくなりやすくなります。 JavaScriptアプリケーションでグローバル状態を管理することを心配する必要がなかった場合は、おめでとうございます。 あなたは幸運な人の一人です。 残りの私たちのために、利用可能な多くの異なるライブラリがあります。

CQRSスタイルの状態管理

Reactの世界で人気のあるライブラリであるCQRS(Command-Query Responsibility Segregation)または Redux に慣れていない場合は、状態を管理するための基本的な原則の概要を以下に示します。

  1. 信頼できる唯一の情報源は、ストアによって管理されています。
  2. 保存された状態は不変です。 直接変更することはできません。
  3. 状態の変更は、レデューサー、現在の状態を引数として取る関数、および状態を実行するアクションによって行われ、アクションが操作を実行した後、完全に新しい状態オブジェクトを返します。

以前の記事では、クライアントとサーバー間のリアルタイム通信用にAngularとSocket.IOを使用して非常に基本的なドキュメントコラボレーションアプリを構築しました。 この種のアプリケーションは、NGXSのような状態管理ライブラリを統合するための完璧なユースケースです。 さまざまなアクターからさまざまな時間に更新が届く場合(この場合、ユーザーは更新を行うことができ、サーバーは更新をプッシュすることができます)、状態コンテナーがあると便利です。 この記事の例の基礎として、前の記事で作成したアプリを使用します。

NGXSを使用したAngularStateManagement

npmから最新の@ngxs/storeパッケージをインストールすることから始めます。

$ npm i @ngxs/store --save

状態とアクションを表すいくつかの新しいES2015クラスを作成します。 これらのファイルをどのように構造化するかはあなた次第です。 わかりやすくするために、state.tsと呼ばれる可能性のある1つのファイルのようにすべてを記述します。 実際には、これらのクラスを独自のファイル構造に分離することをお勧めします。

次に、アプリケーションの状態のスライスを表すいくつかのタイプを作成しましょう。 これらは私たちの州のコンテナになります。 また、状態コンテナに関するメタデータも追加します。

import { State } from '@ngxs/store'

export interface DocumentStateModel {
  id: string;
  doc: string;
}

@State<DocumentStateModel>({
  name: 'document', // required
  defaults: { // optional: DocumentStateModel
    id: '',
    doc: 'Select an existing document or create a new one to get started'
  }
})
export class DocumentState { }

@State<string[]>({
  name: 'documentList',
  defaults: ['']
})
export class DocumentListState { }

アプリケーション全体の状態を、現在のドキュメントのプロパティとドキュメントリストのプロパティを持つ1つの@State装飾クラスに入れることもできますが、2つの異なる状態クラスDocumentStateおよびDocumentListStateは、ストアに必要な数の状態コンテナーを含めることができることを示します。

状態オブジェクトはAngularの依存性注入システムの恩恵を受けるため、状態コンテナーにサービスを注入する場合は、次のことができます。

export class DocumentListState {
  constructor(private documentService: DocumentService) { }
}

行動

アクションを定義することで、ユーザーイベントやアプリで発生したことに対して、州が何をすべきかを非常に宣言的にすることができます。

状態に新しいドキュメントを追加する単純なアクションを定義する方法は次のとおりです。

export class AddDocument {
  static readonly type = '[Document List] Add Document'; // required
}

アクションにはペイロードを含めることもできます。 開いているドキュメントが他の誰かによって編集されており、変更がソケットサーバーによってプッシュダウンされているとします。 コンポーネントは、次のようなアクションをディスパッチすることで応答する場合があります。

export class DocumentEditedFromServer {
  static readonly type = '[Document] Edited From Server';
  constructor(public docText: string) { }
}

状態クラスに戻って、コンポーネントがこれらのアクションをストアにディスパッチしたときに何が起こるかを定義する必要があります。

export class DocumentState {
  @Action(DocumentEditedFromServer)
  editDocument(ctx: StateContext<DocumentStateModel>, action: EditDocument) {
    const state = ctx.getState(); // always returns the freshest slice of state
    ctx.setState({
      ...state, 
      doc: action.docText
    }); // the spread operator is shorthand for Object.assign({}, state, { doc: docText });
  }
}

状態を直接変更することはありません。 代わりに、状態のコピーを作成し、必要なコピーのプロパティを変更して、状態オブジェクト全体を変更したコピーに設定します。

必要に応じて、pipable演算子の1つに状態を設定するObservableを返すことにより、アクションを非同期で実行できます。 例としては、API呼び出しをトリガーし、応答で状態を更新するアクションがあります。 Observableをサブスクライブする必要はありません。 フレームワークがそれを行うので、たとえばtap演算子を使用して、pipeチェーン内の状態を更新します。

選択

ストアからデータを取得する方法はいくつかあります。 まず、コンポーネントで選択したプロパティを定義できます。

@Component({ /*...*/ })
export class DocumentListComponent {
  // Stream of the entire Document List State
  @Select(DocumentListState) documents$: Observable<string[]>;

  // @Select doesn't need a parameter if the name of the 
  // property matches the name of the state you're selecting.    
  @Select() documentList$: Observable<string[]>;

  // You can also use a function to get the slice of state you need.
  @Select(state => state.documentList): Observable<string[]>;
}

メモ化されたセレクター

選択に使用する必要のある特定の機能があり、それを再利用できる場合は、静的選択機能をメモ化することもできます。 あなたの州のクラスでは:

@State<string[]>( /*...*/ )
export class DocumentListState {
  @Selector()
  static lastTenDocuments(state: string[]) {
    return state.slice(-10);
  }
}

これで、コンポーネントでメモ化されたセレクターにアクセスできます。

export class DocumentListComponent {
  @Select(DocumentListState.lastTenDocuments) recentDocuments$: Observable<string[]>;
}

ストアサービスの選択

また、サービスのようにストアをコンポーネントに挿入し、そこから直接選択するオプションもあります。

export class DocumentComponent {
  currentDocument$: Observable<DocumentStateModel>;
  constructor(private store: Store) {
    this.currentDocument$ = this.store.select(state => state.document);
  }
}

また、Observableを使用できない場合は、状態の静的スナップショットを選択するオプションもあります。 スナップショットを選択した時点でのみ状態が更新されることに注意してください。 良いユースケースは、その時点でデータが必要になる可能性があるInterceptorクラスであり、サブスクライブは意味がありません。

this.docId = this.store.selectSnapshot<string>(state => state.document.id)

結論

状態管理に関してNGXSが提供できるものの表面をかろうじてかじっただけです。 NGXSとそのコミュニティが提供する他の高度なパターン、機能、ツール、プラグインは多数あります。これらはこの記事の範囲を超えていますが、今後の記事で検討する予定です。

参考文献