この記事では『ngrx/entity』と『EntityAdapter』について、
- ngrx/entityとEntityAdapterとは
- EntityAdapterをcreateEntityAdapterメソッドで作成する方法
- createEntityAdapterメソッドのselectIdプロパティについて
- createEntityAdapterメソッドのsortComparerプロパティについて
- 初期StateをgetInitialStateメソッドで作成する方法
- 追加のstateプロパティを含める方法
- アダプターメソッドを使用してコレクションをCURD操作する方法
- addOneとは
- addManyとは
- removeOneとは
- removeManyとは
- removeAllとは
- setOneとは
- setManyとは
- setAllとは
- updateOneとは
- updateManyとは
- upsertOneとは
- upsertManyとは
- mapOneとは
- mapとは
- セレクターを使用してコレクションを取得する方法
- selectIdsとは
- selectEntitiesとは
- selectAllとは
- selecttotalとは
- ngrx/entityとEntityAdapterを用いたサンプルコード
などをサンプルコードを用いて分かりやすく説明するように心掛けています。ご参考になれば幸いです。
ngrx/entityとEntityAdapterとは
ngrx/entity
は、Angularの状態管理ライブラリであるNgRx
の一部で、コレクション(エンティティ集合)のCRUD(作成、読み込み、更新、削除)操作を効率的に行うことができるライブラリです。
ngrx/entityは、コレクションのCRUD操作に対するエンティティアダプター(EntityAdapter)を提供しています。
EntityAdapter
はcreateEntityAdapter
メソッドを使用することで生成することができ、EntityAdapter
はコレクションに対して、CURD操作を実行するための多くのアダプターメソッドを持っています(例えば、addOne
メソッドなど)。
また、EntityAdapter
には以下のような初期state
を生成するためのgetInitialState
メソッドを持っています。
{
ids: [],
entities: {}
}
上記の初期state
において、ids
はエンティティのプライマリIDを保持する配列で、entities
はプライマリIDをキーとしてエンティティを格納するオブジェクトです。この構造により、特定のエンティティへのアクセスが高速化され、効率的なCURD操作が可能になります。
例えば、データを2件追加すると、state
は以下のような状態になります。
{
ids: ['001', '002'],
entities: {
'001': {
id: '001',
name: 'addUser001'
},
'002': {
id: '002',
name: 'addUser002'
}
}
}
この例では、ids
配列に2つのID(001
と002
)が格納され、entities
オブジェクトにはこれらのIDをキーとして、各エンティティのデータが格納されています。アダプターメソッドを使用すると、このstate
に対して効率的にCURD操作を行うことができます。
また、ngrx/entityはコレクションから特定のデータを選択するためのセレクターも提供しています。セレクターを使用すると、アプリケーションのさまざまな部分で、このstate
から必要なデータを簡単に取得できます。
ではこれから、実際にサンプルコードを用いてngrx/entity
やEntityAdapter
について詳しく説明します。
EntityAdapterをcreateEntityAdapterメソッドで作成する方法
createEntityAdapter
メソッドを使用してEntityAdapter
を作成するサンプルコードを以下に示します。
import { createEntityAdapter, EntityAdapter } from '@ngrx/entity';
export interface User {
id: string;
name: string;
}
// EntityAdapterの作成
export const userAdapter: EntityAdapter<User> = createEntityAdapter<User>();
上記のサンプルコードでは、createEntityAdapter
メソッドで生成したEntityAdapter
をuserAdapter
変数に割り当てています。
また、createEntityAdapter
メソッドにはselectId
とsortComparer
という2つのプロパティを持つオブジェクトを受け取ることができ、これらを使用すると、プライマリIDを変えたり、ids
やentities
の中身を一定の条件でソートすることができるようになります。
createEntityAdapterメソッドのselectIdプロパティについて
selectId
プロパティを用いると、各エンティティのプライマリIDを決定することができるようになります。デフォルトでは、エンティティのid
プロパティがプライマリIDとして使用されますが、異なるプロパティをプライマリIDとして使用したい場合にはselectId
プロパティを定義します。
selectId
プロパティの値には関数を記述します。例えば、エンティティにおいて、id
プロパティではなくname
プロパティをプライマリIDとして使用したい場合、以下のように記述します。
// EntityAdapterの作成(プライマリIDの変更)
export const userAdapter: EntityAdapter<User> = createEntityAdapter<User>({
selectId: (user) => user.name
});
その結果、state
は以下のような状態になります。
{
users: {
ids: ['addUser001', 'addUser002'], // nameプロパティがプライマリIDになっている
entities: {
addUser001: {
id: '001',
name: 'addUser001'
},
addUser002: {
id: '002',
name: 'addUser002'
}
}
}
}
補足
selectId
プロパティはエンティティにid
プロパティがある場合はオプションになります。今回の場合、指定された型はUser
型であり、id
プロパティがあるので、selectId
は省略可能です。
createEntityAdapterメソッドのsortComparerプロパティについて
sortComparer
プロパティを用いると、ids
やentities
の中身を一定の条件で常にソートすることができるようになります。
sortComparer
プロパティの値には関数を記述します。例えば、name
プロパティの値で降順にソートさせたい場合、以下のように記述します。
// EntityAdapterの作成(nameプロパティの値で降順にソート)
export const userAdapter: EntityAdapter<User> = createEntityAdapter<User>({
sortComparer: (a, b) => b.name.localeCompare(a.name)
});
その結果、state
は以下のような状態になります。
{
users: {
ids: ['002', '001'], // nameプロパティの値で降順になっている
entities: {
'002': {
id: '002',
name: 'addUser002'
},
'001': {
id: '001',
name: 'addUser001'
}
}
}
}
初期StateをgetInitialStateメソッドで作成する方法
getInitialState
メソッドを使用して初期state
(EntityState
型)を作成するサンプルコードを以下に示します。
import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity';
export interface User {
id: string;
name: string;
}
// EntityAdapterの作成
export const userAdapter: EntityAdapter<User> = createEntityAdapter<User>();
// 初期Stateの作成
export const initialState: EntityState<User> = userAdapter.getInitialState();
上記のコードではgetInitialState
メソッドで生成した初期state
をinitialState
変数に割り当てています。以下のような初期state
が生成されます。
{
ids: [],
entities: {}
}
追加のstateプロパティを含める方法
ids
やentities
以外のデータも扱いたい場合には、EntityState
型を拡張して、追加したプロパティの初期値を設定します。サンプルコードを以下に示します。
// EntityState型を拡張してプロパティを追加
export interface UserState extends EntityState<User> {
lastId: number;
loading: boolean;
}
// 初期Stateの作成(追加したプロパティの初期値も設定させる)
export const initialState: UserState = userAdapter.getInitialState({
lastId: 0,
loading: false,
});
例えば、上記のように記述すると、初期state
は以下のようになります。lastId
プロパティとloading
プロパティが追加されました。
{
ids: [],
entities: {},
lastId: 0,
loading: false
}
アダプターメソッドを使用してコレクションをCURD操作する方法
EntityAdapter
はコレクションに対して、CURD操作を実行するために、以下に示すような多くのアダプターメソッドを持っています。後ほど各アダプターメソッドについてサンプルコードを用いて説明します。
アダプターメソッド | 説明 |
addOne | 1つのエンティティをコレクションに追加する |
addMany | 複数のエンティティをコレクションに追加する |
removeOne | 1つのエンティティをコレクションから削除する |
removeMany | 複数のエンティティをコレクションから削除する |
removeAll | 全てのエンティティをコレクションから削除する |
setOne | コレクション内の1つのエンティティを置換または追加する |
setMany | コレクション内の複数のエンティティを置換または追加する |
setAll | 現在のコレクションを提供されたコレクションに全置換する |
updateOne | コレクション内の1つのエンティティを更新する |
updateMany | コレクション内の複数のエンティティを更新する |
upsertOne | コレクション内の1つのエンティティを更新または追加する |
upsertMany | コレクション内の複数のエンティティを更新または追加する |
mapOne | コレクション内の1つのエンティティをマップ関数で更新する |
map | コレクション内の複数のエンティティをマップ関数で更新する |
ではこれから各アダプターメソッドについて順番に詳しく説明します。
addOneとは
addOneは1つのエンティティをコレクションに追加するアダプターメソッドです。なお、すでに同じIDを持つエンティティが存在する場合、新しいエンティティは追加されません。また、addOne
はオブジェクトの形式でエンティティを受け取ります。
addOne
のアダプターメソッドを用いる場合、以下のサンプルコードに示すようにComponent、Action、Reducerを記述します。
Component(app.component.ts)
const addOneUser = { id: '001', name: 'addUser001' };
this.store.dispatch(UserActions.addOneUser({ user: addOneUser }));
Action(user.action.ts)
export const addOneUser = createAction(
'[User] Add One User',
props<{ user: User }>()
);
Reducer(user.reducer.ts)
on(UserActions.addOneUser, (state, action) => {
const { user } = action;
return userAdapter.addOne(user, state);
}),
コレクションが空の状態において、上記のサンプルコードに示すaddOne
を実行した場合、コレクションは以下のようになります。
{
ids: ['001'],
entities: {
'001': {
id: '001',
name: 'addUser001'
}
}
}
addManyとは
addManyは複数のエンティティをコレクションに追加するアダプターメソッドです。なお、すでに同じIDを持つエンティティが存在する場合、新しいエンティティは追加されません。また、addMany
はオブジェクト配列の形式でエンティティを受け取ります。
addMany
のアダプターメソッドを用いる場合、以下のサンプルコードに示すようにComponent、Action、Reducerを記述します。
Component(app.component.ts)
const addManyUsers = [
{ id: '001', name: 'addUser001' },
{ id: '002', name: 'addUser002' },
{ id: '003', name: 'addUser003' },
{ id: '004', name: 'addUser004' },
{ id: '005', name: 'addUser005' },
];
this.store.dispatch(UserActions.addManyUsers({ users: addManyUsers }));
Action(user.action.ts)
export const addManyUsers = createAction(
'[User] Add Many Users',
props<{ users: User[] }>()
);
Reducer(user.reducer.ts)
on(UserActions.addManyUsers, (state, action) => {
const { users } = action;
return userAdapter.addMany(users, state);
}),
コレクションが空の状態において、上記のサンプルコードに示すaddMany
を実行した場合、コレクションは以下のようになります。
{
ids: ['001', '002', '003', '004', '005'],
entities: {
'001': {
id: '001',
name: 'addUser001'
},
'002': {
id: '002',
name: 'addUser002'
},
'003': {
id: '003',
name: 'addUser003'
},
'004': {
id: '004',
name: 'addUser004'
},
'005': {
id: '005',
name: 'addUser005'
}
}
}
removeOneとは
removeOneは指定されたIDに一致する1つのエンティティをコレクションから削除するアダプターメソッドです。removeOne
は数値または文字列でIDを指定します。
removeOne
のアダプターメソッドを用いる場合、以下のサンプルコードに示すようにComponent、Action、Reducerを記述します。
Component(app.component.ts)
const removeId = '001';
this.store.dispatch(UserActions.removeOneUser({ id: removeId }));
Action(user.action.ts)
export const removeOneUser = createAction(
'[User] Remove One User',
props<{ id: string }>()
);
Reducer(user.reducer.ts)
on(UserActions.removeOneUser, (state, action) => {
const { id } = action;
return userAdapter.removeOne(id, state);
}),
上記のサンプルコードに示すremoveOne
を実行した場合、IDが001
のエンティティが削除されます。
removeManyとは
removeManyは指定されたIDの配列に一致する複数のエンティティをコレクションから削除するアダプターメソッドです。removeMany
は配列の形式でIDを指定します。
removeMany
のアダプターメソッドを用いる場合、以下のサンプルコードに示すようにComponent、Action、Reducerを記述します。
Component(app.component.ts)
const removeIds = ['001', '002', '003'];
this.store.dispatch(UserActions.removeManyUsers({ ids: removeIds }));
Action(user.action.ts)
export const removeManyUsers = createAction(
'[User] Remove Many Users',
props<{ ids: string[] }>()
);
Reducer(user.reducer.ts)
on(UserActions.removeManyUsers, (state, action) => {
const { ids } = action;
return userAdapter.removeMany(ids, state);
}),
上記のサンプルコードに示すremoveMany
を実行した場合、IDが001,002,003
のエンティティが削除されます。
removeAllとは
removeAllは全てのエンティティをコレクションから削除するアダプターメソッドです。
removeAll
のアダプターメソッドを用いる場合、以下のサンプルコードに示すようにComponent、Action、Reducerを記述します。
Component(app.component.ts)
this.store.dispatch(UserActions.removeAllUsers());
Action(user.action.ts)
export const removeAllUsers = createAction(
'[User] Remove All Users'
);
Reducer(user.reducer.ts)
on(UserActions.removeAllUsers, (state) => {
return userAdapter.removeAll(state);
}),
上記のサンプルコードに示すremoveAll
を実行した場合、全てのエンティティが削除されます
setOneとは
setOneはコレクション内の1つのエンティティを置換または追加するアダプターメソッドです。なお、すでに同じIDを持つエンティティが存在する場合、そのエンティティは新しいエンティティに置き換えられます。存在しない場合、新しいエンティティを追加します。また、setOne
はオブジェクトの形式でエンティティを受け取ります。
setOne
のアダプターメソッドを用いる場合、以下のサンプルコードに示すようにComponent、Action、Reducerを記述します。
Component(app.component.ts)
const setOneUser = { id: '001', name: 'setUser001' };
this.store.dispatch(UserActions.setOneUser({ user: setOneUser }));
Action(user.action.ts)
export const setOneUser = createAction(
'[User] Set One User',
props<{ user: User }>()
);
Reducer(user.reducer.ts)
on(UserActions.setOneUser, (state, action) => {
const { user } = action;
return userAdapter.setOne(user, state);
}),
コレクションが空の状態において、上記のサンプルコードに示すsetOne
を実行した場合、コレクションは以下のようになります。
{
ids: ['001'],
entities: {
'001': {
id: '001',
name: 'setUser001'
}
}
}
setManyとは
setManyはコレクション内の複数のエンティティを置換または追加するアダプターメソッドです。なお、すでに同じIDを持つエンティティが存在する場合、そのエンティティは新しいエンティティに置き換えられます。存在しない場合、新しいエンティティを追加します。また、setMany
はオブジェクト配列の形式でエンティティを受け取ります。
setMany
のアダプターメソッドを用いる場合、以下のサンプルコードに示すようにComponent、Action、Reducerを記述します。
Component(app.component.ts)
const setManyUsers = [
{ id: '001', name: 'setUser001' },
{ id: '002', name: 'setUser002' },
{ id: '003', name: 'setUser003' },
];
this.store.dispatch(UserActions.setManyUsers({ users: setManyUsers }));
Action(user.action.ts)
export const setManyUsers = createAction(
'[User] Set Many Users',
props<{ users: User[] }>()
);
Reducer(user.reducer.ts)
on(UserActions.setManyUsers, (state, action) => {
const { users } = action;
return userAdapter.setMany(users, state);
}),
コレクションが空の状態において、上記のサンプルコードに示すsetMany
を実行した場合、コレクションは以下のようになります。
{
ids: ['001', '002', '003'],
entities: {
'001': {
id: '001',
name: 'setUser001'
},
'002': {
id: '002',
name: 'setUser002'
},
'003': {
id: '003',
name: 'setUser003'
}
}
}
setAllとは
setAllは現在のコレクションを提供されたコレクションに全置換するアダプターメソッドです。setAll
はオブジェクト配列の形式でエンティティを受け取ります。
setAll
のアダプターメソッドを用いる場合、以下のサンプルコードに示すようにComponent、Action、Reducerを記述します。
Component(app.component.ts)
const setAllUsers = [
{ id: '901', name: 'setUser901' },
{ id: '902', name: 'setUser902' },
{ id: '903', name: 'setUser903' },
];
this.store.dispatch(UserActions.setAllUsers({ users: setAllUsers }));
Action(user.action.ts)
export const setAllUsers = createAction(
'[User] Set All Users',
props<{ users: User[] }>()
);
Reducer(user.reducer.ts)
on(UserActions.setAllUsers, (state, action) => {
const { users } = action;
return userAdapter.setAll(users, state);
}),
上記のサンプルコードに示すsetAll
を実行した場合、コレクションは以下のようになります。
{
ids: ['901', '902', '903'],
entities: {
'901': {
id: '901',
name: 'setUser901'
},
'902': {
id: '902',
name: 'setUser902'
},
'903': {
id: '903',
name: 'setUser903'
}
}
}
updateOneとは
updateOneはコレクション内の1つのエンティティを更新するアダプターメソッドです。なお、すでに同じIDを持つエンティティが存在する場合、そのエンティティは新しいエンティティで更新されます。存在しない場合、新しいエンティティを追加しません(追加を行うのはupsertOne
です)。また、updateOne
はオブジェクトの形式でエンティティを受け取ります。その際、変更する必要のあるプロパティはchanges
プロパティで指定します。
updateOne
のアダプターメソッドを用いる場合、以下のサンプルコードに示すようにComponent、Action、Reducerを記述します。
Component(app.component.ts)
const updatedOneUser = { id: '001', changes: { name: 'updatedUser001' } };
this.store.dispatch(UserActions.updateOneUser({ updateUser: updatedOneUser }));
Action(user.action.ts)
export const updateOneUser = createAction(
'[User] Update One User',
props<{ updateUser: Update<User> }>()
);
Reducer(user.reducer.ts)
on(UserActions.updateOneUser, (state, action) => {
const { updateUser } = action;
return userAdapter.updateOne(updateUser, state);
}),
上記のサンプルコードに示すupdateOne
を実行した場合、IDが001
のエンティティのname
プロパティが更新されます。
updateManyとは
updateManyはコレクション内の複数のエンティティを更新するアダプターメソッドです。なお、すでに同じIDを持つエンティティが存在する場合、そのエンティティは新しいエンティティで更新されます。存在しない場合、新しいエンティティを追加しません(追加を行うのはupsertMany
です)。また、updateMany
はオブジェクト配列の形式でエンティティを受け取ります。その際、変更する必要のあるプロパティはchanges
プロパティで指定します。
updateMany
のアダプターメソッドを用いる場合、以下のサンプルコードに示すようにComponent、Action、Reducerを記述します。
Component(app.component.ts)
const updateManyUsers = [
{ id: '001', changes: { name: 'updatedUser001' } },
{ id: '002', changes: { name: 'updatedUser002' } },
{ id: '003', changes: { name: 'updatedUser003' } },
];
this.store.dispatch(UserActions.updateManyUsers({ updateUsers: updateManyUsers }));
Action(user.action.ts)
export const updateManyUsers = createAction(
'[User] Update Many Users',
props<{ updateUsers: Update<User>[] }>()
);
Reducer(user.reducer.ts)
on(UserActions.updateManyUsers, (state, action) => {
const { updateUsers } = action;
return userAdapter.updateMany(updateUsers, state);
}),
上記のサンプルコードに示すupdateMany
を実行した場合、IDが001,002,003
のエンティティのname
プロパティが更新されます。
upsertOneとは
upsertOneはコレクション内の1つのエンティティを更新または追加するアダプターメソッドです。なお、すでに同じIDを持つエンティティが存在する場合、そのエンティティは新しいエンティティで更新されます。存在しない場合、新しいエンティティを追加します。また、upsertOne
はオブジェクトの形式でエンティティを受け取ります。
upsertOne
のアダプターメソッドを用いる場合、以下のサンプルコードに示すようにComponent、Action、Reducerを記述します。
Component(app.component.ts)
const upsertOneUser = { id: '001', name: 'upsertUser001' };
this.store.dispatch(UserActions.upsertOneUser({ user: upsertOneUser }));
Action(user.action.ts)
export const upsertOneUser = createAction(
'[User] Upsert One User',
props<{ user: User }>()
);
Reducer(user.reducer.ts)
on(UserActions.upsertOneUser, (state, action) => {
const { user } = action;
return userAdapter.upsertOne(user, state);
}),
コレクションが空の状態において、上記のサンプルコードに示すupsertOne
を実行した場合、コレクションは以下のようになります。
{
ids: ['001'],
entities: {
'001': {
id: '001',
name: 'upsertUser001'
}
}
}
upsertManyとは
upsertManyはコレクション内の複数のエンティティを更新または追加するアダプターメソッドです。なお、すでに同じIDを持つエンティティが存在する場合、そのエンティティは新しいエンティティで更新されます。存在しない場合、新しいエンティティを追加します。また、upsertMany
はオブジェクト配列の形式でエンティティを受け取ります。
upsertMany
のアダプターメソッドを用いる場合、以下のサンプルコードに示すようにComponent、Action、Reducerを記述します。
Component(app.component.ts)
const upsertManyUser = [
{ id: '001', name: 'upsertUser001' },
{ id: '002', name: 'upsertUser002' },
{ id: '003', name: 'upsertUser003' },
];
this.store.dispatch(UserActions.upsertManyUsers({ users: upsertManyUser }));
Action(user.action.ts)
export const upsertManyUsers = createAction(
'[User] Upsert Many Users',
props<{ users: User[] }>()
);
Reducer(user.reducer.ts)
on(UserActions.upsertManyUsers, (state, action) => {
const { users } = action;
return userAdapter.upsertMany(users, state);
}),
コレクションが空の状態において、上記のサンプルコードに示すupsertMany
を実行した場合、コレクションは以下のようになります。
{
ids: ['001', '002', '003'],
entities: {
'001': {
id: '001',
name: 'upsertUser001'
},
'002': {
id: '002',
name: 'upsertUser002'
},
'003': {
id: '003',
name: 'upsertUser003'
}
}
}
mapOneとは
mapOneはコレクション内の1つのエンティティをマップ関数で更新するアダプターメソッドです。なお、指定したid
プロパティのエンティティが存在する場合、そのエンティティはマップ関数で更新されます。また、mapOne
はオブジェクトの形式でエンティティを受け取ります。その際、マップ関数で更新したいエンティティはid
プロパティで指定し、map
プロパティには関数を渡します。
mapOne
のアダプターメソッドを用いる場合、以下のサンプルコードに示すようにComponent、Action、Reducerを記述します。
Component(app.component.ts)
const mapOneUser: EntityMapOne<User> = {
id: '001',
map: (entity) => {
return { ...entity, name: 'mappedUser001' };
},
};
this.store.dispatch(UserActions.mapOneUser({ entityMap: mapOneUser }));
Action(user.action.ts)
export const mapOneUser = createAction(
'[User] Map One User',
props<{ entityMap: EntityMapOne<User> }>()
);
Reducer(user.reducer.ts)
on(UserActions.mapOneUser, (state, action) => {
const { entityMap } = action;
return userAdapter.mapOne(entityMap, state);
}),
上記のサンプルコードに示すmapOne
を実行した場合、IDが001
のエンティティのname
プロパティが更新されます。
mapとは
mapはコレクション内の複数のエンティティをマップ関数で更新するアダプターメソッドです。map
は関数の形式でエンティティを受け取ります。
map
のアダプターメソッドを用いる場合、以下のサンプルコードに示すようにComponent、Action、Reducerを記述します。
Component(app.component.ts)
const mapUsers: EntityMap<User> = (entity) => {
if (entity.id === '001' || entity.id === '002') {
return { ...entity, name: 'mappedUser' + entity.id };
} else {
return entity;
}
};
this.store.dispatch(UserActions.mapUsers({ entityMap: mapUsers }));
Action(user.action.ts)
export const mapUsers = createAction(
'[User] Map Users',
props<{ entityMap: EntityMap<User> }>()
);
Reducer(user.reducer.ts)
on(UserActions.mapUsers, (state, action) => {
const { entityMap } = action;
return userAdapter.map(entityMap, state);
}),
上記のサンプルコードに示すmap
を実行した場合、IDが001,002
のエンティティのname
プロパティが更新されます。
セレクターを使用してコレクションを取得する方法
EntityAdapter
はコレクションを取得するために、以下に示すような多くのセレクターを持っています。後ほど各セレクターについてサンプルコードを用いて説明します。
セレクター | 説明 |
selectIds | idの一覧を配列で取得する |
selectEntities | エンティティの一覧をオブジェクトで取得する |
selectAll | エンティティの一覧をオブジェクト配列で取得する |
selectTotal | エンティティの総数を取得する |
ではこれから一例として、以下に示すようなコレクションがある状態において、各セレクターの説明を説明します。
{
users: {
ids: ['002', '001'],
entities: {
'002': {
id: '002',
name: 'addUser002'
},
'001': {
id: '001',
name: 'addUser001'
}
}
}
}
selectIdsとは
selectIdsはidの一覧を配列で取得するセレクターです。取得した際の型はstring[]
またはnumber[]
になります。
selectIds
のセレクターを用いる場合、以下のサンプルコードに示すようにComponentとSelectorを記述します。
Component(app.component.ts)
userIds$ = this.store.select(UserSelectors.selectUserIds);
constructor(private store: Store<UserState>) {
this.userIds$.subscribe((data) => console.log(data));
}
// ログ出力
// ['001', '002']
Selector(user.selector.ts)
export const selectUserIds = createSelector(selectUserState, selectIds);
selectEntitiesとは
selectEntitiesはエンティティの一覧をオブジェクトで取得するセレクターです。取得した際の型は{[id: string]: User}
になります(User
の箇所はプログラムによって異なります。今回のサンプルコードの場合にはUser
型になります)。
selectEntities
のセレクターを用いる場合、以下のサンプルコードに示すようにComponentとSelectorを記述します。
Component(app.component.ts)
userEntities$ = this.store.select(UserSelectors.selectUserEntities);
constructor(private store: Store<UserState>) {
this.userEntities$.subscribe((data) => console.log(data));
}
// ログ出力
// {
// '001': {
// id: '001',
// name: 'addUser001'
// },
// '002': {
// id: '002',
// name: 'addUser002'
// }
// }
Selector(user.selector.ts)
export const selectUserIds = createSelector(selectUserState, selectIds);
selectAllとは
selectAllはエンティティの一覧をオブジェクト配列で取得するセレクターです。取得した際の型はUser[]
になります(型はプログラムによって異なります。今回のサンプルコードの場合にはUser[]
型になります)。
selectAll
のセレクターを用いる場合、以下のサンプルコードに示すようにComponentとSelectorを記述します。
Component(app.component.ts)
allUsers$ = this.store.select(UserSelectors.selectAllUsers);
constructor(private store: Store<UserState>) {
this.allUsers$.subscribe((data) => console.log(data));
}
// ログ出力
// [
// {id: '001', name: 'addUser001'},
// {id: '002', name: 'addUser002'}
// ]
Selector(user.selector.ts)
export const selectUserIds = createSelector(selectUserState, selectIds);
selectAll
はEntityState
型をオブジェクト配列(今回の場合はUser[]
型)に変換してくれる非常に便利なセレクターです。
selecttotalとは
selecttotalはエンティティの総数を取得するセレクターです。取得した際の型はnumber
になります。
selecttotal
のセレクターを用いる場合、以下のサンプルコードに示すようにComponentとSelectorを記述します。
Component(app.component.ts)
totalUsers$ = this.store.select(UserSelectors.selectTotalUsers);
constructor(private store: Store<UserState>) {
this.totalUsers$.subscribe((data) => console.log(data));
}
// ログ出力
// 2
Selector(user.selector.ts)
export const selectUserIds = createSelector(selectUserState, selectIds);
ngrx/entityとEntityAdapterを用いたサンプルコード
counter-app //アプリ名
├─src
│ ├─app
│ │ ├─app.module.ts //★Appモジュール
│ │ ├─app.component.ts //★Appコンポーネント
│ │ ├─user.actions.ts //★Action
│ │ ├─user.reducer.ts //★Reducer
│ │ └─user.selector.ts //★Selector
│ ├─main.ts
│ ├─index.html
│ └─styles.css
├─angular.json
├─package-lock.json
├─package.json
├─tsconfig.app.json
├─tsconfig.json
└─node_modules
ディレクトリ構成は上図のようになっています。上記の★マークで示したファイルを作成する必要があります。
app.module.ts
import { NgModule, isDevMode } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { StoreModule } from '@ngrx/store';
import { userReducer } from './user.reducer';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, StoreModule.forRoot({ users: userReducer })],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
app.component.ts
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { EntityMap, EntityMapOne } from '@ngrx/entity';
import { Observable } from 'rxjs';
import * as UserSelectors from './user.selector';
import * as UserActions from './user.action';
import { User } from './user.reducer';
import { UserState } from './user.reducer';
@Component({
selector: 'app-root',
template: `
<div><button (click)="addOneUser()">ユーザーの追加(addOne)</button>:「ID:001」のユーザーを1人追加する</div>
<div><button (click)="addManyUsers()">ユーザーの複数追加(addMany)</button>:「ID:001~005」のユーザーを5人追加する</div>
<div><button (click)="removeOneUser()">ユーザーの削除(removeOne)</button>:「ID:001」のユーザーを1人削除する</div>
<div><button (click)="removeManyUsers()">ユーザーの複数削除(removeMany)</button>:「ID:001~003」のユーザーを3人削除する</div>
<div><button (click)="removeAllUsers()">ユーザーの全削除(removeAll)</button>:ユーザーを全削除する</div>
<div><button (click)="setOneUser()">ユーザーの置換(setOne)</button>:「ID:001」のユーザーを1人置換する</div>
<div><button (click)="setManyUsers()">ユーザーの複数置換(setMany)</button>:「ID:001~003」のユーザーを3人置換する</div>
<div><button (click)="setAllUsers()">ユーザーの全置換(setAll)</button>:ユーザーを全置換する</div>
<div><button (click)="updateOneUser()">ユーザーの更新(updateOne)</button>:「ID:001」のユーザーを1人更新する</div>
<div><button (click)="updateManyUsers()">ユーザーの複数更新(updateMany)</button>:「ID:001~003」のユーザーを3人更新する</div>
<div><button (click)="upsertOneUser()">ユーザーのアップサート(upsertOne)</button>:「ID:001」のユーザーを1人アップサートする</div>
<div><button (click)="upsertManyUsers()">ユーザーの複数アップサート(upsertMany)</button>:「ID:001~003」のユーザーを3人アップサートする</div>
<div><button (click)="mapOneUser()">ユーザーをマップ関数で更新(mapOne)</button>:「ID:001」のユーザーを1人マップ関数で更新する</div>
<div><button (click)="mapUsers()">ユーザーをマップ関数で複数更新(map)</button>:「ID:001~002」のユーザーをマップ関数で更新する</div>
<h3>ユーザーIDのリスト</h3>
<ul>
<li *ngFor="let userId of userIds$ | async">{{ userId }}</li>
</ul>
<h3>ユーザーのリスト</h3>
<ul>
<li *ngFor="let user of allUsers$ | async">ID:{{ user.id }} NAME:{{ user.name }}</li>
</ul>
<h3>ユーザーの総数</h3>
<p>{{ totalUsers$ | async }}</p>
`,
})
export class AppComponent {
userIds$ = this.store.select(UserSelectors.selectUserIds) as Observable<string[]>;
userEntities$ = this.store.select(UserSelectors.selectUserEntities) as Observable<{ [id: string]: User }>;
allUsers$ = this.store.select(UserSelectors.selectAllUsers) as Observable<User[]>;
totalUsers$ = this.store.select(UserSelectors.selectTotalUsers) as Observable<number>;
constructor(private store: Store<UserState>) {
// デバッグ用
this.userIds$.subscribe((data) => console.log(data));
this.userEntities$.subscribe((data) => console.log(data));
this.allUsers$.subscribe((data) => console.log(data));
this.totalUsers$.subscribe((data) => console.log(data));
}
// addOne
addOneUser() {
const addOneUser = { id: '001', name: 'addUser001' };
this.store.dispatch(UserActions.addOneUser({ user: addOneUser }));
}
// addMany
addManyUsers() {
const addManyUsers = [
{ id: '001', name: 'addUser001' },
{ id: '002', name: 'addUser002' },
{ id: '003', name: 'addUser003' },
{ id: '004', name: 'addUser004' },
{ id: '005', name: 'addUser005' },
];
this.store.dispatch(UserActions.addManyUsers({ users: addManyUsers }));
}
// removeOne
removeOneUser() {
const removeId = '001';
this.store.dispatch(UserActions.removeOneUser({ id: removeId }));
}
// removeMany
removeManyUsers() {
const removeIds = ['001', '002', '003'];
this.store.dispatch(UserActions.removeManyUsers({ ids: removeIds }));
}
// removeAll
removeAllUsers() {
this.store.dispatch(UserActions.removeAllUsers());
}
// setOne
setOneUser() {
const setOneUser = { id: '001', name: 'setUser001' };
this.store.dispatch(UserActions.setOneUser({ user: setOneUser }));
}
// setMany
setManyUsers() {
const setManyUsers = [
{ id: '001', name: 'setUser001' },
{ id: '002', name: 'setUser002' },
{ id: '003', name: 'setUser003' },
];
this.store.dispatch(UserActions.setManyUsers({ users: setManyUsers }));
}
// ユーザーの全置換 (setAll)
setAllUsers() {
const setAllUsers = [
{ id: '901', name: 'setUser901' },
{ id: '902', name: 'setUser902' },
{ id: '903', name: 'setUser903' },
];
this.store.dispatch(UserActions.setAllUsers({ users: setAllUsers }));
}
// updateOne
updateOneUser() {
const updatedOneUser = { id: '001', changes: { name: 'updatedUser001' } };
this.store.dispatch(UserActions.updateOneUser({ updateUser: updatedOneUser }));
}
// updateMany
updateManyUsers() {
const updateManyUsers = [
{ id: '001', changes: { name: 'updatedUser001' } },
{ id: '002', changes: { name: 'updatedUser002' } },
{ id: '003', changes: { name: 'updatedUser003' } },
];
this.store.dispatch(UserActions.updateManyUsers({ updateUsers: updateManyUsers }));
}
// upsertOne
upsertOneUser() {
const upsertOneUser = { id: '001', name: 'upsertUser001' };
this.store.dispatch(UserActions.upsertOneUser({ user: upsertOneUser }));
}
// upsertMany
upsertManyUsers() {
const upsertManyUser = [
{ id: '001', name: 'upsertUser001' },
{ id: '002', name: 'upsertUser002' },
{ id: '003', name: 'upsertUser003' },
];
this.store.dispatch(UserActions.upsertManyUsers({ users: upsertManyUser }));
}
// mapOne
mapOneUser() {
const mapOneUser: EntityMapOne<User> = {
id: '001',
map: (entity) => {
return { ...entity, name: 'mappedUser001' };
},
};
this.store.dispatch(UserActions.mapOneUser({ entityMap: mapOneUser }));
}
//map
mapUsers() {
const mapUsers: EntityMap<User> = (entity) => {
if (entity.id === '001' || entity.id === '002') {
return { ...entity, name: 'mappedUser' + entity.id };
} else {
return entity;
}
};
this.store.dispatch(UserActions.mapUsers({ entityMap: mapUsers }));
}
}
user.action.ts
import { createAction, props } from '@ngrx/store';
import { EntityMap, EntityMapOne, Update } from '@ngrx/entity';
import { User } from './user.reducer';
// addOne
export const addOneUser = createAction(
'[User] Add One User',
props<{ user: User }>()
);
// addMany
export const addManyUsers = createAction(
'[User] Add Many Users',
props<{ users: User[] }>()
);
// removeOne
export const removeOneUser = createAction(
'[User] Remove One User',
props<{ id: string }>()
);
// removeMany
export const removeManyUsers = createAction(
'[User] Remove Many Users',
props<{ ids: string[] }>()
);
// removeAll
export const removeAllUsers = createAction(
'[User] Remove All Users'
);
// setOne
export const setOneUser = createAction(
'[User] Set One User',
props<{ user: User }>()
);
// setMany
export const setManyUsers = createAction(
'[User] Set Many Users',
props<{ users: User[] }>()
);
// setAll
export const setAllUsers = createAction(
'[User] Set All Users',
props<{ users: User[] }>()
);
// updateOne
export const updateOneUser = createAction(
'[User] Update One User',
props<{ updateUser: Update<User> }>()
);
// updateMany
export const updateManyUsers = createAction(
'[User] Update Many Users',
props<{ updateUsers: Update<User>[] }>()
);
// upsertOne
export const upsertOneUser = createAction(
'[User] Upsert One User',
props<{ user: User }>()
);
// upsertMany
export const upsertManyUsers = createAction(
'[User] Upsert Many Users',
props<{ users: User[] }>()
);
// mapOne
export const mapOneUser = createAction(
'[User] Map One User',
props<{ entityMap: EntityMapOne<User> }>()
);
// map
export const mapUsers = createAction(
'[User] Map Users',
props<{ entityMap: EntityMap<User> }>()
);
user.reducer.ts
import { createReducer, on } from '@ngrx/store';
import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity';
import * as UserActions from './user.action';
export interface User {
id: string;
name: string;
}
export const userAdapter: EntityAdapter<User> = createEntityAdapter<User>({
//selectId: (user) => user.name,
// sortComparer: (a, b) => b.name.localeCompare(a.name),
});
// EntityState型を拡張してプロパティを追加
export interface UserState extends EntityState<User> {}
// 追加したプロパティの初期値を設定
export const initialState: UserState = userAdapter.getInitialState();
export const userReducer = createReducer(
initialState,
// addOne
on(UserActions.addOneUser, (state, action) => {
const { user } = action;
return userAdapter.addOne(user, state);
}),
// addMany
on(UserActions.addManyUsers, (state, action) => {
const { users } = action;
return userAdapter.addMany(users, state);
}),
// removeOne
on(UserActions.removeOneUser, (state, action) => {
const { id } = action;
return userAdapter.removeOne(id, state);
}),
// removeMany
on(UserActions.removeManyUsers, (state, action) => {
const { ids } = action;
return userAdapter.removeMany(ids, state);
}),
// removeAll
on(UserActions.removeAllUsers, (state) => {
return userAdapter.removeAll(state);
}),
// setOne
on(UserActions.setOneUser, (state, action) => {
const { user } = action;
return userAdapter.setOne(user, state);
}),
// setMany
on(UserActions.setManyUsers, (state, action) => {
const { users } = action;
return userAdapter.setMany(users, state);
}),
// setAll
on(UserActions.setAllUsers, (state, action) => {
const { users } = action;
return userAdapter.setAll(users, state);
}),
// updateOne
on(UserActions.updateOneUser, (state, action) => {
const { updateUser } = action;
return userAdapter.updateOne(updateUser, state);
}),
// updateMany
on(UserActions.updateManyUsers, (state, action) => {
const { updateUsers } = action;
return userAdapter.updateMany(updateUsers, state);
}),
// upsertOne
on(UserActions.upsertOneUser, (state, action) => {
const { user } = action;
return userAdapter.upsertOne(user, state);
}),
// upsertMany
on(UserActions.upsertManyUsers, (state, action) => {
const { users } = action;
return userAdapter.upsertMany(users, state);
}),
// mapOne
on(UserActions.mapOneUser, (state, action) => {
const { entityMap } = action;
return userAdapter.mapOne(entityMap, state);
}),
// map
on(UserActions.mapUsers, (state, action) => {
const { entityMap } = action;
return userAdapter.map(entityMap, state);
})
);
user.selector.ts
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { UserState, userAdapter } from './user.reducer';
export const selectUserState = createFeatureSelector<UserState>('users');
const { selectIds, selectEntities, selectAll, selectTotal } = userAdapter.getSelectors();
export const selectUserIds = createSelector(selectUserState, selectIds);
export const selectUserEntities = createSelector(selectUserState, selectEntities);
export const selectAllUsers = createSelector(selectUserState, selectAll);
export const selectTotalUsers = createSelector(selectUserState, selectTotal);
本記事のまとめ
この記事では『ngrx/entity』と『EntityAdapter』について、以下の内容を説明しました。
- ngrx/entityとEntityAdapterとは
- EntityAdapterをcreateEntityAdapterメソッドで作成する方法
- createEntityAdapterメソッドのselectIdプロパティについて
- createEntityAdapterメソッドのsortComparerプロパティについて
- 初期StateをgetInitialStateメソッドで作成する方法
- 追加のstateプロパティを含める方法
- アダプターメソッドを使用してコレクションをCURD操作する方法
- addOneとは
- addManyとは
- removeOneとは
- removeManyとは
- removeAllとは
- setOneとは
- setManyとは
- setAllとは
- updateOneとは
- updateManyとは
- upsertOneとは
- upsertManyとは
- mapOneとは
- mapとは
- セレクターを使用してコレクションを取得する方法
- selectIdsとは
- selectEntitiesとは
- selectAllとは
- selecttotalとは
- ngrx/entityとEntityAdapterを用いたサンプルコード
お読み頂きありがとうございました。