Redux Toolkit の AsyncThunk をさわってみる のつづき。
EntityAdapter とは
- createEntityAdapter | Redux Toolkit
- NgRx メンテナによって作成された@ngrx/entity ライブラリから、Redux Toolkit に移植された API
- Redux Toolkit v1.3.0 で追加された
- https://github.com/reduxjs/redux-toolkit/pull/374
- エンティティ操作用のアダプターを生成してくれ、CRUD(create, read, update, delete)操作の機能を提供してくれる
エンティティとは
- アプリケーション内の一意のタイプのデータオブジェクトを指すために使用される用語
- 以下のよう一意の ID 値を持っているオブジェクトを格納したコレクションに対し、CRUD 操作を行う
{
tasks: [
{ id: 1, title: "aaa", done: false },
{ id: 2, title: "bbb", done: false },
];
}
- プリミティブなオブジェクトだけ扱える(クラスインスタンスは扱えない)
アダブターと初期 state の生成
- Redux Toolkit の
createEntityAdapter
でアダプターを生成し、アダプターのgetInitialState()
で初期 state を得る
interface Task {
id: number;
title: string;
done: boolean;
}
const tasksAdapter = createEntityAdapter<Task>();
const taskInitialEntityState = tasksAdapter.getInitialState();
getInitialState()
で、次のような初期 state が返される
{
ids: [];
entities: {
}
}
- 例えば、アダプターを使ってデータ2件追加すると、state は次のような状態になる
{
ids: [1, 2]
entities: {
{
1: {id: 1, title: 'aaa', done: false},
2: {id: 2, title: 'bbb', done: false}
}
}
}
ids
やentities
以外のデータも扱いたい場合はどうする?EntityState<Task>
型を拡張して、追加したプロパティの初期値を設定する
interface TaskEntityState extends EntityState<Task> {
lastId: number;
}
// 初期state生成時に、追加したプロパティの値を指定する
const taskInitialEntityState: TaskEntityState = tasksAdapter.getInitialState({
lastId: 2,
});
id
ではなくtaskId
という名前で、識別子を管理したい場合は?- アダプター生成時に次のように、
id
を取得する関数を指定する
- アダプター生成時に次のように、
const tasksAdapter = createEntityAdapter<Task>({
selectId: (task) => task.taskId,
});
ids
やentities
の中身を、一定の条件で常にソートしたい場合は?- アダプター生成時に次のように、
sortComparer()
関数を指定する
- アダプター生成時に次のように、
const tasksAdapter = createEntityAdapter<Task>({
sortComparer: (a, b) => a.title.localeCompare(b.title),
});
state の追加・更新・削除
getInitialState()
で生成したEntityState
をinitialState
に割り当てる
const todoSlice = createSlice({
name: 'todo',
initialState: taskInitialEntityState,
reducers: { ... }
...
- 例えば
EntityState.entities
をまるまるセットしたい場合は、アダプターのsetAll()
メソッドをreducers
で使う
const todoSlice = createSlice({
...
reducers: {
setTasks(state, action: PayloadAction<{ tasks: Task[] }>) {
tasksAdapter.setAll(state, action.payload.tasks);
},
}
})l
- 複数の
EntityState
を扱いたい場合はどうする?- 必ずしも
EntityState
がinitialState
になっている必要はないので、initialState
に複数のEntityState
を持たせれば良い
- 必ずしも
const todoSlice = createSlice({
name: 'todo',
initialState: {
taskEntityState: taskInitialEntityState,
barEntityState: barInitialEntityState,
hoge: {...},
fuga: {...},
},
reducers: {
setTasks(state, action: PayloadAction<{ tasks: Task[] }>) {
tasksAdapter.setAll(state, action.payload.tasks);
},
setBars(state, action: PayloadAction<{ bars: Bar[] }>) {
barsAdapter.setAll(state, action.payload.bars);
},
}
});
- setAll 以外にも以下メソッドがある
- addOne、addMany
- removeOne、removeMany
- updateOne、updateMany
- upsertOne、upsertMany
EntityState の参照
- 普通に以下のようにコンポーネント側から参照できるけど、この場合メモ化はされない
const { entities } = useSelector(
(state: RootState) => state.todo.taskEntityState
);
- Redux + React におけるメモ化の必要性とは?
- Redux の state は「アプリケーション全体に対して 1 つのツリーオブジェクト(Single source of truth)」 という原則がある
- このツリーが大きくなった時、各コンポーネントが関心を持つのは、ツリーの中にある一部の state に限定される事が多い
- しかし、無関係なツリーの一部が更新されても、上記
useSelector
の処理は毎度走ってしまう- この時、
useSelector
で配列走査とかしてると無駄な負荷がかかってしまう - なので参照している一部の state に変化がない限りは、cache を返すというメモ化処置が有効になる
- この時、
- Redux には「Reselect」というメモ化ライブラリがあり、EntityAdapter が提供するセレクター関数は、このライブラリの
createSelector
関数で作られているためメモ化された参照になる - 最初に、参照する state ツリーを限定して、セレクター関数郡を取得する
export const taskSelectors = tasksAdapter.getSelectors(
(state: RootState) => state.todo.taskEntityState
);
- Entity を全件取得するには
selectors.selectAll()
を使う
// コンポーネント側
const tasks = useSelector(taskSelectors.selectAll);
return (
<div>
{tasks.map((task) => (
...
))}
</div>
);
- useSelector は RootState を selector 関数に渡し、
selectors.selectAll()
はRootState
のみを受け取るので、上記処理は以下と同じ
const tasks = useSelector((state: RootState) => {
return taskSelectors.selectAll(state);
});
selectors.selectById()
は、RootState
とid
を引数にとるので次のように書く
const task = useSelector((state: RootState) => {
return taskSelectors.selectById(state, 1);
});
- 以下のようなセレクターがある
- selectIds
- selectEntities
- selectAll
- selectTotal
- selectById
メモ化して独自のセレクタを書く
- 以下
createSelector
を使って、独自セレクターをメモ化する
function createSelector<S, R1, T>(
selector: Selector<S, R1>,
combiner: (res: R1) => T,
): OutputSelector<S, T, (res: R1) => T>;
- 上記
selector
は任意の数のだけ指定でき、combiner
はselector
の返却値を引数として取るので、以下のように書ける
const barSelector = createSelector(
(_state: RootState, bar: number) => bar,
(_state: RootState) => 2,
(_state: RootState) => 3,
(a, b, c) => a + b + c
);
const bar = barSelector(store.getState(), 10);
console.log(bar); // 15
- 最後に追加されたタスクの ID は、
taskEntityState.lastId
で管理されていると仮定した場合、このタスクのデータを取得するには以下のように書ける
export const lastTaskSelector = createSelector(
(state: RootState) => state,
(state: RootState) => state.todo.taskEntityState.lastId,
taskSelectors.selectById
);
// コンポーネント側
const lastTask = useSelector(lastTaskSelector);
- コンポーネント側から state 以外の引数を指定するセレクターを書くには、以下のように
paylaod
でまとめて書くこともできる(まとめず個別に渡す場合は、createSelector で指定する selector 関数を引数の数の分だけ書くことになる)
export const tasksSearchSelectorByTitle = createSelector(
(payload: { state: RootState; title: string }) => payload,
(payload) => {
return taskSelectors.selectAll(payload.state).find((item) => {
return item.title.match(payload.title);
});
}
);
// コンポーネント側
const tasks = useSelector((state: RootState) => {
return tasksSearchSelectorByTitle({ state, title: "hoge" });
});
もう 1 つの selector の書き方
- さっきは以下のように、ツリーの参照箇所を絞ってセレクター関数郡を取得した
export const taskSelectors = tasksAdapter.getSelectors(
(state: RootState) => state.todo.taskEntityState
);
- 以下のように扱う state の対象を絞らないで書くこともできる
const taskSelectors = tasksAdapter.getSelectors();
- 但し、この場合は利用するアダプターに対応する state を都度、渡す必要がある
// コンポーネント側
const tasks = useSelector((state: RootState) => {
return taskSelectors.selectAll(state.todo.taskEntityState); // アダプターに対応するstate
});
// セレクターの生成
export const allTaskSelector = createSelector(
(state: RootState) => state.todo.taskEntityState, // アダプターに対応するstate
taskSelectors.selectAll
);