普段はフロントエンドエンジニアとして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を使ってより実践的なアプリを作成してみたいと思います。
■ 参考文献