目次
今回利用するサンプルアプリケーションについて
Selectorのメモ化を検証するにあたり、今回はTodo管理機能とカウンタ機能を組み合わせたアプリケーションを利用します。
アプリケーションの仕様は以下の通りです。
- Stateにはtodo(Todoの内容)、visibilityFilter(todoをフィリタリングする識別子)、count(カウンターの値)が保存されている
- 「+Count」「-Count」をクリックするとcountを増減させるActiongaが発行される
- 「all」「completed」「incomplete」をクリックするとvisibilityFilterを変更するActionが発行される
- 「Add Todo」をクリックするとTodoを追加するActiongaが発行される
アプリケーションに保存されているStateの具体例は以下の通りです。
{
todos: {
allIds: [
1,
2
],
byId: {
'1': {
id: 1,
content: 'go somewhere',
completed: true
},
'2': {
id: 2,
content: 'eat something',
completed: false
}
}
},
visibilityFilter: 'completed',
count: 4
}
Selectorについて
Selectorとは、ReduxのStore内のStateから特定のデータを取得する関数のことをいいます。
Selectorを作成することにより、コンポーネントはSelectorを呼ぶだけでStateから必要なデータが抽出できます。
SelectorはあくまでStateからデータを抽出するための関数にすぎないため、Selectorの実装に別途ライブラリをインストールする必要はありません。
Selectorの利用方法
コンポーネントでSelectorを利用する方法は以下の2つがあります。
- useSelectorを利用する
- connectの第一引数(mapStateToProps)を利用する
具体的なサンプルコードを紹介します。
ここでは例として、Stateに保存されているvisibilityFilter
を取得するSelectorをコンポーネントで利用するケースについて考えてみます。
useSelectorを利用した実装は以下の通りです。
import { FC } from "react";
import { AppState } from "../store/index";
import { VisibilityFilterTypes } from "../types/state/visibilityFilter";
import { useSelector } from "react-redux";
const VisibilityFilters: FC = () => {
const activeFilter: VisibilityFilterTypes = useSelector(
(state: AppState) => state.visibilityFilter
);
return (
<>
(略)
</>
);
};
export default VisibilityFilters;
connectを利用した実装は以下の通りです。
import { FC } from "react";
import { connect } from "react-redux";
import { AppState } from "../store/index";
import { VisibilityFilterTypes } from "../types/state/visibilityFilter";
type VisibilityFiltersProps = {
activeFilter: VisibilityFilterTypes;
}
const VisibilityFilters: FC<VisibilityFiltersProps> = ({ activeFilter }) => {
return (
<>
(略)
</>
);
};
const mapStateToProps = (state: AppState) => {
return { activeFilter: state.visibilityFilter };
};
export default connect(mapStateToProps)(VisibilityFilters);
Selectorの特性について
Selectorは任意のアクションが発行(dispatch)されるたびに毎回実行されるという特性があります。これはuseSelectorを利用している場合もconnectを利用している場合も起こります。
上記の特性について、冒頭で紹介したアプリケーションを利用して確認してみます。
以下は、Stateに保存されているtodoの中から画面上に表示するtodoを抽出するSelectorです。
src/selectors/todo.ts
import { AppState } from "../store/index";
import { VisibilityFilterTypes } from "../types/state/visibilityFilter";
import { VISIBILITY_FILTERS } from "../types/constants/visibilityFilterType";
import { TodoState, TodoItem } from "../types/state/todos";
const todos = (state: AppState): TodoState => state.todos;
const selectTodoIds = (state: AppState): number[] => todos(state).allIds;
const selectTodosById = (state: AppState) => todos(state).byId;
const selectTodos = (state: AppState): TodoItem[] =>
selectTodoIds(state).map((id) => selectTodosById(state)[id]);
export const selectTodosByVisibilityFilter = (
state: AppState,
visibilityFilter: VisibilityFilterTypes
): TodoItem[] => {
// Selectorが実行されたことを確認するためのデバッグログ
console.log("Selector is called");
const allTodos = selectTodos(state);
switch (visibilityFilter) {
case VISIBILITY_FILTERS.COMPLETED:
return allTodos.filter((todo) => todo.completed);
case VISIBILITY_FILTERS.INCOMPLETE:
return allTodos.filter((todo) => !todo.completed);
case VISIBILITY_FILTERS.ALL:
default:
return allTodos;
}
};
Selectorの呼び出し側の実装は以下の通りです。
src/components/TodoList.tsx
import { FC } from "react";
import { AppState } from "../store/index";
import { selectTodosByVisibilityFilter } from "../selectors/todo";
import { TodoItem } from "../types/state/todos";
import { useSelector } from "react-redux";
const TodoList: FC = () => {
const todos: TodoItem[] = useSelector((state: AppState) => {
const { visibilityFilter } = state;
return selectTodosByVisibilityFilter(state, visibilityFilter);
});
return (
<>
(略)
</>
);
};
export default TodoList;
上記のSelectorはcountを利用しないため、countに関するActionには依存していません。
しかし、カウントボタンをクリックした時もログをみてわかるようにSelectorが実行されています。
Reselectについて
Reselectはメモ化されたSelectorを作成するためのライブラリです。
Reselectが提供するcreateSelectorというAPIを利用することでSelectorがメモ化できます。
Redux ToolkitにはReselectがデフォルトでインストールされています。
ReselectはReduxアプリケーションで利用されることが多いですが、Redux以外のJavaScriptアプリケーションでも利用可能です。
先ほど紹介した「Selectorが毎回実行されてしまう」という問題点は、ReselectのcreateSelectorを利用してSelectorをメモ化することで解決できます。
createSelectorの概要と基本構文
createSelectorの定義はcreateSelector(...inputSelectors | [inputSelectors], resultFunc, selectorOptions?)
です。
inputSelectors
はStore内の特定のStateを抽出するSelectorです。
resultFunc
はinputSelectors
によって取得されたStateを引数として受け取り、データ加工などの計算処理を行う関数です。
なお、Redux『createSelector Overview』ではresultFunc
のことをoutput selector
という名称で紹介しています。
createSelectorはinputSelectors
の戻り値、つまりresultFunc
の処理に必要なStateの値が厳密等価(===
)において等しい場合、前回の計算結果を返します。
複数のStateを引数に利用してresultFunc
の計算をする場合、つまりinputSelectors
を複数定義する場合は、カンマ区切りもしくは配列でSelectorを並べます。
参考: よくあるcreateSelectorの間違った利用方法
inputSelectors
はStore内の特定のStateを抽出するSelectorですので、stateをそのまま返す(state => state
)関数は不適切です。
resultFunc
はinputSelectors
の戻り値を利用して変換処理を行う部分ですので、以下のようにinputSelectors
の結果をそのまま返すのは不適切です。
// inputSelectorの結果をそのまま返してるだけの無意味なcreateSelector
const brokenSelector = createSelector(
state => state.todos,
todos => todos
)
createSelectorのサンプルコード
先ほど紹介した「Stateに保存されているtodoの中から画面上に表示するtodoを抽出するSelector」を例に、createSelectorのサンプルコードを紹介します。
再掲: createSelectorの導入前のSelector
src/selectors/todo.ts
import { AppState } from "../store/index";
import { VisibilityFilterTypes } from "../types/state/visibilityFilter";
import { VISIBILITY_FILTERS } from "../types/constants/visibilityFilterType";
import { TodoState, TodoItem } from "../types/state/todos";
const todos = (state: AppState): TodoState => state.todos;
const selectTodoIds = (state: AppState): number[] => todos(state).allIds;
const selectTodosById = (state: AppState) => todos(state).byId;
const selectTodos = (state: AppState): TodoItem[] =>
selectTodoIds(state).map((id) => selectTodosById(state)[id]);
export const selectTodosByVisibilityFilter = (
state: AppState,
visibilityFilter: VisibilityFilterTypes
): TodoItem[] => {
// Selectorが実行されたことを確認するためのデバッグログ
console.log("Selector is called");
const allTodos = selectTodos(state);
switch (visibilityFilter) {
case VISIBILITY_FILTERS.COMPLETED:
return allTodos.filter((todo) => todo.completed);
case VISIBILITY_FILTERS.INCOMPLETE:
return allTodos.filter((todo) => !todo.completed);
case VISIBILITY_FILTERS.ALL:
default:
return allTodos;
}
};
Selectorの呼び出し側の実装は以下の通りです。
src/components/TodoList.tsx
import { FC } from "react";
import { AppState } from "../store/index";
import { selectTodosByVisibilityFilter } from "../selectors/todo";
import { TodoItem } from "../types/state/todos";
import { useSelector } from "react-redux";
const TodoList: FC = () => {
const todos: TodoItem[] = useSelector((state: AppState) => {
const { visibilityFilter } = state;
return selectTodosByVisibilityFilter(state, visibilityFilter);
});
return (
<>
(略)
</>
);
};
export default TodoList;
createSelectorの導入後のSelector
src/selectors/todo.ts
import { AppState } from "../store/index";
import { VISIBILITY_FILTERS } from "../types/constants/visibilityFilterType";
import { TodoState } from "../types/state/todos";
import { createSelector } from "@reduxjs/toolkit";
import { selectVisibilityFilter } from "./visibilityFilter";
const todos = (state: AppState): TodoState => state.todos;
const selectTodoIds = createSelector([todos], (todos) => todos.allIds);
const selectTodosById = createSelector([todos], (todos) => todos.byId);
const selectTodos = createSelector(
[selectTodoIds, selectTodosById],
(todoIds, todosById) => todoIds.map((id) => todosById[id])
);
export const selectTodosByVisibilityFilter = createSelector(
[selectTodos, selectVisibilityFilter],
(todos, visibilityFilter) => {
// Selectorが実行されたことを確認するためのデバッグログ
console.log("Selector is called");
switch (visibilityFilter) {
case VISIBILITY_FILTERS.COMPLETED:
return todos.filter((todo) => todo.completed);
case VISIBILITY_FILTERS.INCOMPLETE:
return todos.filter((todo) => !todo.completed);
case VISIBILITY_FILTERS.ALL:
default:
return todos;
}
}
);
src/selectors/visibilityFilter.ts
import { AppState } from "../store/index";
import { VisibilityFilterTypes } from "../types/state/visibilityFilter";
export const selectVisibilityFilter = (state: AppState): VisibilityFilterTypes =>
state.visibilityFilter;
Selectorの呼び出し側の実装は以下の通りです。
src/components/TodoList.tsx
import { FC } from "react";
import { selectTodosByVisibilityFilter } from "../selectors/todo";
import { TodoItem } from "../types/state/todos";
import { useSelector } from "react-redux";
const TodoList: FC = () => {
const todos: TodoItem[] = useSelector(selectTodosByVisibilityFilter);
return (
<>
(略)
</>
);
};
export default TodoList;
createSelectorを導入したことで、ログからわかる通りアプリケーションのカウントボタンをクリックした時にはtodoに関するSelector(getTodosByVisibilityFilter
)は実行されなくなります。
さいごに
Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!