Angular

【Angular】@ngrx/component-storeの使い方

マナビト
マナビト
こんにちはマナビトです。
普段はフロントエンドエンジニアとしてJavaScript/TypeScript/Aangular/GraphQLを
メインに開発業務を行なっています。

Angularの中でも比較的新しい@ngrx/component-storeというライブラリについて実務で触る機会があったので、記事にしていきたいと思います。

@ngrx/component-storeとは?

@ngrx/component-storeはコンポーネント間のデータ連携と、データの一元管理が容易に行うことができる状態管理ライブラリです。AngularではReduxを参考に開発されたNgRXといった状態管理ライブラリが既に存在しますが、よりコンポーネント間のデータ連携に特化した仕様となっています。

コンポーネント間のデータ連携としてAngularでは「BehaviorSubect」も提供されていますが、信頼できるデータを一元管理するといった意味では@ngrx/component-storeが圧倒的に優れています。

また、コンポーネントがスコープ外(ユーザーがアプリケーションの別の部分に移動するなど)になると、全てのデータがクリーンアップされるため、開発車はデータをクリーンアップする方法を考える必要がないのも特徴です。

■ 公式ドキュメント

先ほど述べたNgRXですが、NgRXには実装されており@ngrx-component-storeにはない機能がいくつかあります。

一点目はAction機能です。Actionはユーザーがクリックするなどの動作で発行され、ストアのStateを変更する為のメッセージの役割を果たします。
また、CRUD操作を簡単にしてくれるEntitiyの機能も存在しません。(NgRXの詳細についてはまた改めて記事にしたいと思います。)

より多くのコンポーネント間でデータ連携を行う必要がある場合や、Entitiyを使用する必要がある場合は、NgRXを採用した方が良いかと思います。

@ngrx/component-storeの実装

それでは、@ngrx/component-storeで簡単なカウンターアプリを実装してみたいと思います。

バージョンは以下の通りです。
・Angular: ver12.2.0
・ngrx/component-store:ver13.0.2

まずは、npmでインストールします。

npm install @ngrx/component-store --save

■ ng addの場合

ng add @ngrx/component-store@latest

component-storeの雛形

まずはcomponent-storeの雛形を作成していきます。これから記載するソースコードは全て、Githubに記載してます。

■ component-store.ts

import { Injectable } from '@angular/core';
import { ComponentStore } from '@ngrx/component-store';
import { Observable } from 'rxjs';

export interface Counter {
    count: number;
}

@Injectable()
export class CounterStore extends ComponentStore<counterNumber> {

  constructor() {
    super({ count: 0 });
  }

}

@component-storeをインポートすることで、componentStoreを取り込むことができます。今回は見やすさ重視の為、interfaceを同じファイルに記載してますが、基本的にinterfaceは別のフォルダにまとめた方が良いと思います。

また、@InjectableデコレータにprovideInは記載していません。providedInを記載してしまうと、他のサービスクラスやコンポーネントに依存性の注入が可能となる為、データの独立性が損なわれてしまう為です。

componentStoreではconstructorのsuperに、更新したいデータを格納し初期値を設定します。

Counterを作成

データの型は下記の通りです。

■ count.ts

export interface Counter {
    count: number;
}

Selector及びupdaterを実装

データを取得、更新するためにSelector、updaterを作成していきます。

component-store.ts

import { Injectable } from '@angular/core';
import { ComponentStore } from '@ngrx/component-store';
import { Counter } from './models/counter';
import { Observable } from 'rxjs';

@Injectable()
export class CounterStore extends ComponentStore<Counter> {

  constructor() {
    super({ count: 0 });
  }

  // read state
  readonly count$: Observable<number> = this.select((state) => state.count);

  // update state
  readonly add = this.updater((state) => ({ count: state.count + 1 }));

}

インターフェイスで定義しているcountはselectorを使用して取得します。公式ドキュメントにはselectorは以下の通り定義されており、projectorには純粋関数を指定します。また戻り値はObservableとなります。

  /**
   * Creates a selector.
   *
   * This supports combining up to 4 selectors. More could be added as needed.
   *
   * @param projector A pure projection function that takes the current state and
   *   returns some new slice/projection of that state.
   * @param config SelectConfig that changes the behavior of selector, including
   *   the debouncing of the values until the state is settled.
   * @return An observable of the projector results.
   */
  select<R>(projector: (s: T) => R, config?: SelectConfig): Observable<R>;

データを更新する場合は、setstateまたはupdaterを使用します。今回はupdaterを使用してaddが呼び出される度にcountのデータが一つ更新されるようにします。

View及びCompoentの実装

今度はView側とComponent側を実装していきます。

app.component.ts

import { Component } from '@angular/core';
import { CounterStore } from './component-store';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.sass'],
  providers: [CounterStore],
})
export class AppComponent {

  constructor(private readonly counterStore: CounterStore){}

  readonly count$ = this.counterStore.count$

  onClickAddButton(): any{
    this.counterStore.add();
  }

}

onClickAddButton()メゾットがクリックされる毎にcountStoreのaddが呼び出され、データが更新されます。

app.component.html

<p>Counter</p>
<div>This count is {{ count$ | async }}</div>
<div>
  <button (click)="onClickAddButton()">count</button>
</div>

selectorで取得した値は非同期のObservableとなるため、htmlにasyncパイプを付け足します。


これでcountorアプリの作成ができました。
次回は、component-storeを使ってより実践的なアプリを作成してみたいと思います。

■ 参考文献