@ngrx/entity 使い,エンティティの管理を行うフィーチャーモジュールを実装する時,別のモジュールからエンティティの操作を行うためには,専用のアクションを用意する必要がある.この場合,アプリケーション全体を通して,似たようなアクションが大量に存在してしまう.
例えば,以下のような UserStoreModule
を考える
// user-store.module.ts
export interface UserState {
entity: EntityState<{ id: string; name: string }>;
}
@NgModule({
imports: [
StoreModule.forFeature('user', { reducers })
]
})
export class UserStoreModule {}
この場合,他のモジュール等から User
エンティティに関する操作を行いたい場合,以下のようにアクションをディスパッチしなければならないため,アクションを提供する必要がある.
例えば,APIからのレスポンスをストアに登録したい,という場合は以下のようになる.
@Effect()
something$ = this.action$.pipe(
// some operators...
switchMap(() =>
httpClient.get<User[]>('/users').pipe(
mergeMap(users => [
upsertUsers({ users }),
fetchUsersSuccess({ users })
])
)
)
);
User
以外のエンティティを追加する時,同様にエンティティの操作を行うアクションを追加する必要がある.結果的に,アプリケーション全体を通して,似たようなアクションが大量に存在してしまうことになる.
あるアクションを実行する際に,同時にエンティティの操作も行いたいケースを考える. これを実現するために,アクションをディスパッチする際に,同時にエンティティ操作を行うアクションをディスパッチする方法が考えられる. しかし,アクションをディスパッチする箇所で,常にエンティティの更新が行われるとは限らないため アクションのディスパッチ側でエンティティの操作を担保することは望ましくない.
例えば,先のケースの以下のコードについて.
mergeMap(users => [
upsertUsers({ users }),
fetchUsersSuccess({ users })
])
このケースでは,User
をAPIから取得したため,fetchUsersSuccess
をディスパッチすると同時にupsertUsers
を発火することでエンティティの更新を行っている.
しかし, fetchUsersSuccess
をディスパッチする際に upsertUsers
が ディスパッチされるかは,アクションをディスパッチする側に委ねられており,複数箇所で fetchUsersSuccess
を使用する場合には問題となる.
この問題を解決するために,entity-metareducers
を提案する.
entity-metareducers
では,エンティティの管理に責任を持つモジュールを作成する.
(アプリケーションの規模によって,これを分割しても良い)
import { metaReducers } from 'entity-metareducers';
export interface CollectionState {
Users: EntityState<User>,
Articles: EntityState<Articles>,
Comments: EntityState<Comments>,
}
export actionCreators = createActionCreators<CollectionState>();
@NgModule({
imports: [
StoreModule.forFeature('collection', { metaReducers })
]
})
export class CollectionStoreModule {}
entity-metareducers
は,以下のEntityAdapter
のメソッドに対応する関数を提供する.
- withAddOne
- withAddMany
- withAddAll
- withRemoveOne
- withRemoveOne
- withRemoveMany
- withRemoveMany
- withRemoveMany
- withRemoveAll
- withUpdateOne
- withUpdateMany
- withUpsertOne
- withUpsertMany
- withMap
これらの関数は,アクションを返す関数を引数にとり,新しい関数を返す.
返された関数は,引数に取った関数が返すアクションに,entityMeta
プロパティを追加したものを返す.
entityMeta
プロパティは,そのアクションがディスパッチされる際に行われるエンティティの操作を表す.
例えば,以下のように使用される.
export const fetchUsersSuccess = actionCreators.withUpsertMany(
// コレクションの名前
'Users',
// アクション(fetchUsersSuccess) を引数にとり,エンティティを返す
action => action.users,
// 通常のアクション
createAction('[User] Fetch Users Success', props<{ users: User[] }>)
);
// `fetchUsersSuccess` は以下のようなアクションを返すようになる
//
// {
// type: '[User] Fetch Users Success',
// users: [...],
// entityMeta: {
// actions: [
// {
// type: '[Entity] UpsertMany',
// entities: [...],
// collection: 'User'
// }
// ]
// }
// }
entityMeta
が付加されたアクションは,entity-metareducers
モジュールの metaReducer を通すことで,
元々のアクションに加え,エンティティの操作に関するアクションがディスパッチされる.
metaReducerの簡易的な実装は以下のようになる.
function metaReducer(reducer) {
return (action, state) => {
if (!('entityMeta' in action)) {
return reducer(action, state);
}
const entityActions = action.entityMeta.actions;
const newState = entityActions.reduce((prev, cur) => reducer(cur, prev), state);
return reducer(action, newState);
}
}