AngularのReactiveライブラリである@ngrxに手を出したので、まずはIonic + @ngrx/storeでカウンターアプリを作ってみました。ある程度Fluxアーキテクチャ周りの知識が必要になります。@ngrx/storeのバージョンは4.0.0です。

@ngrx/storeについてざっくりと

@ngrx/storeはGithub
RxJS powered state management for Angular applications, inspired by Redux
とあるように、Reduxでやるような状態管理をAngularが使用しているRxJSの力を使って実現しましょうというものです。ということでRedux同様、Store, Reducer, Actionでもって状態管理をします。ReduxではMiddleware等で実現していた非同期処理は、@ngrxでは@ngrx/effectsで処理されます。

Ionicで新規プロジェクトの作成

まずはIonicで新規プロジェクトを作成します。Ionicをすでにインストールしている場合はionic, cordovaのインストールは必要ありません。npmからyarnに乗り換えた人は適宜読み替えてください。 ちなみにインストール先でionic infoして得られる環境情報は以下の通りです。
cli packages:

    @ionic/cli-plugin-ionic-angular : 1.4.0 (...)
    @ionic/cli-utils                : 1.6.0 (...)
    ionic (Ionic CLI)               : 3.6.0 (...)

local packages:

    @ionic/app-scripts : 2.1.3
    Ionic Framework    : ionic-angular 3.6.0

System:

    Node       : v8.2.1
    OS         : macOS Sierra
    Xcode      : Xcode 8.3.3 Build version 8E3004b
    ios-deploy : 1.9.1
    ios-sim    : 6.0.0
    npm        : 5.3.0

Stateがプリミティブ型の例

@ngrxのドキュメントページに載っている、stateがプリミティブ型であるような一番簡単な例から始めます。

State/Action/Reducerの定義

簡単なカウンターアプリなのでディレクトリ構成をどうするかは特に考えず、以下の内容のcounter.tsファイルをsrc/appに追加します。 reducer関数はReduxと同様に初期状態とactionを引数にとります。関数内ではaction.typeに応じてswitchで処理を分けます。ここではstateを1だけ増やす・1だけ減らす・0にリセットするの3つだけです。それ以外はstateをそのまま返します。カウンターアプリのように簡単な場合だとpayloadの出番はありませんが、実用的なアプリになるとstate.payloadを新しい状態の一部として返します。

AppModuleにStoreを登録

作成したreducerをsrc/app/app.module.tsで読み込み、StoreModule.forRootで登録します。 ここではStoreModule.forRoot({counter: reducer})とすることで、stateの中で、読み込んだreducerで管理できる部分にcounterという名前をつけています。コンポーネントからはstore.select('counter')でこのstate(今回はプリミティブな数字)を取得することができるようになります。

Homeコンポーネントに実装

コンポーネントからstoreを利用する準備ができたので、Homeコンポーネントに諸々の処理を書いていきます。 home.htmlにはボタンとcount$を表示する要素を追加します。 パイプを通してasyncに渡すことで、observableな非同期オブジェクトをそのままテンプレートに表示させています。 スタイルは適当につけておきます。

ローカルサーバで確認

ionic serve でローカルサーバを立ち上げてhttp://localhost:8100/にアクセスし、こんな感じでアプリが表示されれば成功です。  

複数のreducerに対応させる

ここまではreducerが1つだけのシンプルなものでしたが、実際のアプリではreducerが複数存在します。ということでそれに対応するためにコードを少し修正します。

reducers.tsの作成

まずは以下の内容のsrc/app/reducers.tsを作成します。 先ほどsrc/pages/home/home.tsで定義していたAppStateをこちらに移しています。また、@ngrx/storeのActionReducerMapとしてreducersを定義しています。ここにcounter.tsから読み込んだreducerを登録します。 2つ以上のreducerを登録するときには のようにreducersの中に追加していきます。

app.module.tsでreducers.tsの読み込み

先ほどStoreModule.forRoot({counter: reducer})としてreducerを登録していましたが、今度はreducers.tsを読み込んでreducersを丸ごとStoreModule.forRootに渡します。

home.tsの修正

あとはhome.tsの修正です。 変更点はhome.ts内で定義していたinterface AppState ...を削除して、代わりにreducers.tsからAppStateを読み込んでいます。これで先ほどと同様にカウンターアプリが動作することが確認できます。

Meta Reducersを追加してstateとactionのログをとってみる

複数のreducerに対応させたときに、アプリ内のreducerをreducersとして一つにまとめました。ここではStoreModule.forRootにmeta reducersを追加して、stateとactionのログをとってみます。手を加えるのはapp.module.tsです。 新たにdebug関数を定義してmetaReducers配列に渡し、StoreModule.forRootのオプションで登録しています。 metaReducerはreducerを引数にとり、新たなreducerを返す関数として定義します。reducerを返すため、return function(...)の引数は当然(state, action)です。このstateがreducerが実行される前のstateです。ここではreducerを実行後のstateをnextStateとし、 – reducerが実行される前のstate – ディスパッチされたaction – reducerが実行された後のstate をコンソールに表示しています。reducerはstateを返すため、最後にはreturn nextStateで新たなstateを返しています。 @ngrxでは自分でわざわざロガーを登録しなくても、@ngrx/store-devtoolsという開発者用のデバッグツールが用意されています。使い方は簡単で、npm install @ngrx/store-devtools でインストールした後、 のように、imports配列にStoreDevtoolsModule.instrument()を追加するだけです。あとはブラウザにRedux DevTools Extensionをインストールすれば、Redux DevToolsで状態遷移を追いかけることができます。

Actionにpayloadをもたせてタイプ付けをする

簡単なカウンターアプリでは必要ありませんが、多くの場合ではactionをディスパッチする際にpayloadをもたせてreducerに情報を渡します。ところが@ngrx/storeのActionインターフェースはtypeプロパティしかもっていないため、これだけだとpayloadをもたせることができません。 また複数のreducerを実装するようになると、actionの衝突を避ける必要が出てきます。ということで次はactionにpayloadをもたせてタイプ付けします。

Actionの実装

まずは以下の内容のsrc/app/actions.tsファイルを作成します。 インターフェースとして定義されているActionに対してIncrementDecrementResetクラスを実装しています。最後にはTypeScriptのUnion type(直和型)としてすべてのactionをexportしています。 今まではResetでカウンターを0にリセットしていましたが、それでは面白くないので、Resetクラスのconstructorでpayloadをもたせています。これでactionをディスパッチする際にpayloadを追加できるようになります。

Reducerの修正

次にcounter.tsを修正します。 今までは@ngrx/storeからActionを読み込んでいましたが、ここではactions.tsからActionを呼んでいます。またリセット時に0を返していましたが、action.payloadが返るように変更しています。

home.tsの修正

最後にhome.tsの修正です。 actionをディスパッチする際にactions.tsで定義したクラスを生成しています。ここではResetに3を渡しているので、リセットボタンをクリックするとカウンターが3に初期化されるのがアプリで確認できます。

Stateをプリミティブ型からオブジェクト型にする

多くのアプリではstateはnumberやstringなどのプリミティブ型ではなく、それらを含むオブジェクト型です。ということでstateをnumberからobjectにしてみます。

counter.tsの修正

まずはcounte.tsです。 export interface Stateが加わっています。ここでstateをオブジェクトとして定義しています。中にはnumber型のcountプロパティをもたせています。この変更に伴い、initialStateやreducerの返り値の型がnumberからStateに変更されます。 また、一番下にcountプロパティを抜き出すための関数を新たに定義しています。あとでcreateSelectorにこの関数を渡します。

reducers.tsの修正

次はreducers.tsです。 AppStateの中身がcounter: numberからcounter: fromCounter.Stateに変更されています。また、最後の2行でcountプロパティを取得するためのgetCounterCountを定義しています。そのために、まずはstateの中でcounter部分だけを取り出したgetCounterStatecreateFeatureSelectorを使って定義しています。そしてcreateSelectorの第一引数にそれを、第二引数にcounter.tsで定義したgetCount関数を渡しています。これでgetCounterCountを使ってcountプロパティの値をコンポーネント側で参照することができます。

home.tsの修正

最後はhome.tsです。 constructorに渡されるstoreの型がfromRoot.AppStateに、store.selectの引数に渡されるのが’counter’からfromRoot.getCounterCountに変更されています。 以上の修正を加えることで、アプリの挙動はまったく同じままでstateをプリミティブなnumber型からオブジェクトに変更することができます。先ほど導入したmetaReducers、もしくはStoreDevtoolsModuleで、stateがただの数字から{count: 0}のようなオブジェクトに変更されているのが確認できます。

まとめ

今回は最初の入り口としてIonic + @ngrx/storeでカウンターアプリを作ってみました。FluxやRedux, Vuexでの基本的な処理の流れを知っていれば敷居はかなり低いと思います。state, action, reducerを用意して、StoreModule.forRootにreducersを渡すだけです。 実用的なアプリになるとAPIへ問い合わせたりするような処理が加わります。その場合には@ngrx/storeに加えて@ngrx/effectsを追加することになります。@ngrx/effectsではRxJSがバリバリ使われることになるので、そちらはRxJSに馴染みがないとハードルが高いかもしれません。