NgRxを試した所感
NgRxはAngularにおける状態管理のためのライブラリです。
ng-conf 2018でも状態管理関連のセッションはNgRxを前提とした内容が多く、デファクトスタンダードなのではと思います。
今回は、Tour of Heroesに対してNgRxを適用してみました。
こちらがコードです。
github.com
今更ながら、Reduxライクな状態管理について勉強しているので、その一貫としてAngularにReduxを適用したいというのが動機です。
NgRxの使い方については末尾の参考を見てもらうとして、以下、使用してみた所感です。
良い面
状態管理に対して厳格な設計を適用できる
NgRxを使うことでActions、ReducersといったReduxの要素をどのように実装すれば良いか明確な指針が示されます。作る側はその通りに作れば良いので、設計を考える必要が無くなります。
Reduxを適用するにあたり考慮しなければいけないこととして、HTTP通信などの外部システムとのやりとりをどうするかといった問題があります。NgRxではこの問題をEffectsで対処しています。
Effectsを使うことで、ActionsからReducersへの処理の流れをそのままにして、HTTPリクエストの処理とHTTPレスポンスの処理とを下記のようなコードで制御することができます。
hero.effects.ts
@Effect() loadHeroes$ = this.actions$.pipe( ofType( HeroActionTypes.HeroesRequestedDashboard ,HeroActionTypes.HeroesRequestedHeroes), mergeMap(() => this.heroService.getHeroes()), map(heroes => new HeroesLoaded({heroes})) );
状態の可視化が楽になる
Redux DevToolsを使い、Action毎に状態を可視化できるのが便利です。
悪い面
RxJS+Redux+αといった作りが重い
Reduxに対して色々な要素が盛り込まれているので理解して、使いこなすまでに学習コストがかかるライブラリという印象です。
個人的には特にEffectsやSelectors、EntityといったNgRx独自の要素に戸惑いました。
コードの見通しに注意を払う必要がある
状態管理のために必要な要素が多いので、コード量が多くなり、見通しが悪くなります。
モジュールを分けるなどして見通しを良くする工夫を意識する必要がありそうです。
今回試しに作成したReducersはコードの見通しが悪いと感じています。
下記コードの他にEffectsも関係しているので複雑です。
index.ts
import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity'; import { ActionReducerMap, createSelector, MetaReducer } from '@ngrx/store'; import { storeFreeze } from 'ngrx-store-freeze'; import { environment } from '../../environments/environment'; import { Hero } from '../hero'; import { HeroActions, HeroActionTypes } from '../hero.actions'; export const adapter: EntityAdapter<Hero> = createEntityAdapter<Hero>(); export interface HeroState extends EntityState<Hero> {} export interface SearchedHeroState extends EntityState<Hero> {} export interface AppState { hero: HeroState, searchedHero: SearchedHeroState } export const reducers: ActionReducerMap<AppState> = { hero: heroReducer, searchedHero: searchedHeroReducer }; export const initialHeroState: HeroState = adapter.getInitialState(); export const initialSearchedHeroState: SearchedHeroState = adapter.getInitialState(); export function heroReducer(state = initialHeroState, action: HeroActions): HeroState { switch (action.type) { case HeroActionTypes.HeroesLoaded: return adapter.addAll(action.payload.heroes, {...state}); case HeroActionTypes.HeroLoaded: case HeroActionTypes.HeroAddedSuccess: return adapter.addOne(action.payload.hero, state); case HeroActionTypes.HeroUpdated: return adapter.updateOne(action.payload.hero, state); case HeroActionTypes.HeroDeleted: return adapter.removeOne(action.payload.hero.id, state); default: return state; } } export function searchedHeroReducer(state = initialSearchedHeroState, action: HeroActions): SearchedHeroState { switch (action.type) { case HeroActionTypes.HeroesSearchLoaded: return adapter.addAll(action.payload.heroes, {...state}); default: return state; } } export const selectHeroState = (state: AppState) => state.hero; export const selectHeroes = createSelector(selectHeroState, adapter.getSelectors().selectAll); export const selectHeroesById = (id: number) => createSelector( selectHeroState, (state: HeroState) => state.entities[id]); export const selectSearchedHeroState = (state: AppState) => state.searchedHero; export const selectSearchedHeroes = createSelector(selectSearchedHeroState, adapter.getSelectors().selectAll); export const metaReducers: MetaReducer<AppState>[] = !environment.production ? [storeFreeze] : [];
注意点
Actionsの定義
NgRxだと、ユーザの画面操作、Web APIからのレスポンスといったActionを細かに定義することが求められます。これはRedux DevToolsを使った状態の変化を追跡するのに役に立ちます。
しかし、Actionを細かに定義するのは面倒と感じることもあるように思います。単純に数が増えるのでActionをどのように定義すれば良いか迷います。
Actionにおけるtypeの一例が下記です。
hero.actions.ts
export enum HeroActionTypes { HeroesRequestedDashboard = '[Dashboard Page] Heroes Requested', HeroesRequestedHeroes = '[Heroes Page] Heroes Requested', HeroesLoaded = '[Heroes API] Heroes Loaded', HeroRequested = '[Hero Page] Hero Requested', HeroLoaded = '[Heroes API] Hero Loaded', HeroAdded = '[Heroes Page] Hero Added', HeroUpdated = '[Hero Detail Page] Hero Updated', HeroDeleted = '[Heroes Page] Hero Deleted', HeroAddedSuccess= '[Heroes API] Hero Added Success', HeroesSearchRequested = '[Hero Search Page] Heroes Search Requested', HeroesSearchLoaded = '[Heroes API] Heroes Search Loaded' }
イベントの発生源とイベントの内容を明確に定義することが推奨されています。
まとめ
NgRxは大規模で複雑な状態管理を求められるアプリに対して適用するというのが分かりやすい判断基準だと思いますが、コストに見合った恩恵を得られるのか十分に検討する必要がありそうです。
今回の取り組みで既存のServiceを使いつつ、NgRxが適用可能なことを確認できました。
AngularはServiceを使うことである程度の状態管理ができます。まずはServiceで状態管理を始めて、状態管理が複雑になってきたらNgRxを適用するのが開発効率を考えると筋が良いように思います。