ngrx/entityとEntityAdapterとは?「使い方」などを詳しく解説!

この記事では『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)を提供しています。

EntityAdaptercreateEntityAdapterメソッドを使用することで生成することができ、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(001002)が格納され、entitiesオブジェクトにはこれらのIDをキーとして、各エンティティのデータが格納されています。アダプターメソッドを使用すると、このstateに対して効率的にCURD操作を行うことができます。

また、ngrx/entityはコレクションから特定のデータを選択するためのセレクターも提供しています。セレクターを使用すると、アプリケーションのさまざまな部分で、このstateから必要なデータを簡単に取得できます。

ではこれから、実際にサンプルコードを用いてngrx/entityEntityAdapterについて詳しく説明します。

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メソッドで生成したEntityAdapteruserAdapter変数に割り当てています。

また、createEntityAdapterメソッドにはselectIdsortComparerという2つのプロパティを持つオブジェクトを受け取ることができ、これらを使用すると、プライマリIDを変えたり、idsentitiesの中身を一定の条件でソートすることができるようになります。

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プロパティを用いると、idsentitiesの中身を一定の条件で常にソートすることができるようになります。

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メソッドで生成した初期stateinitialState変数に割り当てています。以下のような初期stateが生成されます。

{
  ids: [],
  entities: {}
}

追加のstateプロパティを含める方法

idsentities以外のデータも扱いたい場合には、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操作を実行するために、以下に示すような多くのアダプターメソッドを持っています。後ほど各アダプターメソッドについてサンプルコードを用いて説明します。

アダプターメソッド説明
addOne1つのエンティティをコレクションに追加する
addMany複数のエンティティをコレクションに追加する
removeOne1つのエンティティをコレクションから削除する
removeMany複数のエンティティをコレクションから削除する
removeAll全てのエンティティをコレクションから削除する
setOneコレクション内の1つのエンティティを置換または追加する
setManyコレクション内の複数のエンティティを置換または追加する
setAll現在のコレクションを提供されたコレクションに全置換する
updateOneコレクション内の1つのエンティティを更新する
updateManyコレクション内の複数のエンティティを更新する
upsertOneコレクション内の1つのエンティティを更新または追加する
upsertManyコレクション内の複数のエンティティを更新または追加する
mapOneコレクション内の1つのエンティティをマップ関数で更新する
mapコレクション内の複数のエンティティをマップ関数で更新する

ではこれから各アダプターメソッドについて順番に詳しく説明します。

addOneとは

addOne1つのエンティティをコレクションに追加するアダプターメソッドです。なお、すでに同じIDを持つエンティティが存在する場合、新しいエンティティは追加されません。また、addOneはオブジェクトの形式でエンティティを受け取ります。

addOneのアダプターメソッドを用いる場合、以下のサンプルコードに示すようにComponentActionReducerを記述します。

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のアダプターメソッドを用いる場合、以下のサンプルコードに示すようにComponentActionReducerを記述します。

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のアダプターメソッドを用いる場合、以下のサンプルコードに示すようにComponentActionReducerを記述します。

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のアダプターメソッドを用いる場合、以下のサンプルコードに示すようにComponentActionReducerを記述します。

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のアダプターメソッドを用いる場合、以下のサンプルコードに示すようにComponentActionReducerを記述します。

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のアダプターメソッドを用いる場合、以下のサンプルコードに示すようにComponentActionReducerを記述します。

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のアダプターメソッドを用いる場合、以下のサンプルコードに示すようにComponentActionReducerを記述します。

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のアダプターメソッドを用いる場合、以下のサンプルコードに示すようにComponentActionReducerを記述します。

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のアダプターメソッドを用いる場合、以下のサンプルコードに示すようにComponentActionReducerを記述します。

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のアダプターメソッドを用いる場合、以下のサンプルコードに示すようにComponentActionReducerを記述します。

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のアダプターメソッドを用いる場合、以下のサンプルコードに示すようにComponentActionReducerを記述します。

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のアダプターメソッドを用いる場合、以下のサンプルコードに示すようにComponentActionReducerを記述します。

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のアダプターメソッドを用いる場合、以下のサンプルコードに示すようにComponentActionReducerを記述します。

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のアダプターメソッドを用いる場合、以下のサンプルコードに示すようにComponentActionReducerを記述します。

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はコレクションを取得するために、以下に示すような多くのセレクターを持っています。後ほど各セレクターについてサンプルコードを用いて説明します。

セレクター説明
selectIdsidの一覧を配列で取得する
selectEntitiesエンティティの一覧をオブジェクトで取得する
selectAllエンティティの一覧をオブジェクト配列で取得する
selectTotalエンティティの総数を取得する

ではこれから一例として、以下に示すようなコレクションがある状態において、各セレクターの説明を説明します。

{
  users: {
    ids: ['002', '001'],
    entities: {
      '002': {
        id: '002',
        name: 'addUser002'
      },
      '001': {
        id: '001',
        name: 'addUser001'
      }
    }
  }
}

selectIdsとは

selectIdsはidの一覧を配列で取得するセレクターです。取得した際の型はstring[]またはnumber[]になります。

selectIdsのセレクターを用いる場合、以下のサンプルコードに示すようにComponentSelectorを記述します。

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のセレクターを用いる場合、以下のサンプルコードに示すようにComponentSelectorを記述します。

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のセレクターを用いる場合、以下のサンプルコードに示すようにComponentSelectorを記述します。

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);

selectAllEntityState型をオブジェクト配列(今回の場合はUser[]型)に変換してくれる非常に便利なセレクターです。

selecttotalとは

selecttotalはエンティティの総数を取得するセレクターです。取得した際の型はnumberになります。

selecttotalのセレクターを用いる場合、以下のサンプルコードに示すようにComponentSelectorを記述します。

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を用いたサンプルコード

お読み頂きありがとうございました。