Edited at

Reduxのreselectとは

More than 1 year has passed since last update.

Reduxにおけるreducer分割とcombineReducersについて - Qiita

こちらの記事を読んで、私も(Redux は勉強し始めたばかりだけど)似たような感想を持っていて

最後のコメントに書かれていた reselect というライブラリが気になったので、調べてみました。

取り急ぎ、こちらの公式ドキュメントおよび YouTube にあった動画を見ておおざっぱに理解したつもりでいるので、自分なりに整理してみます。


解決したい課題

ドキュメントに書いてあったのと同じく、Redux の basics チュートリアル を例に考えてみる。

このチュートリアルを通して作成したのは、以下のようなフィルタ機能つきの Todo アプリだった。

このアプリにおいて、中央の Todo のリストは

現在登録されているすべての todos を、現在選択されている visibilityFilter でフィルタリングしたものだけを表示している。

該当のコードはこんな感じだった。


containers/VisibleTodoList.js(抜粋)

const getVisibleTodos = (todos, filter) => {

switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
}
}

const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}

...

const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)

export default VisibleTodoList


この時の問題として、getVisibleTodos()関係する state.todos もしくは state.visibilityFilter に更新があったかどうかに関わらず state が更新されるたびに実行されるので、フィルタリング処理の計算コストが高かった場合パフォーマンスに影響が出てしまう。

Redux には「state はアプリケーション全体で1つのツリーオブジェクトである(Single source of truth)」という原則があるが、

state のツリーが巨大になったとき、たいてい各コンポーネントで関心のある state はツリーの中のほんの一部にしかすぎないはずなのに

無関係なツリーの更新によって計算処理が何度も実行されてしまうのは無駄である。

また、動画の方では別の問題点も挙げていた。



(キャプチャは 動画 より引用)

問題の1つは、内部で保持しているデータ(state)の構造をコンポーネント側が知っていなきゃいけないということと、

もう1つは、フィルタリング処理などのロジック部分をコンポーネント側に持たせてしまうとそのロジックを別のところで再利用することが難しくなるということだった。

今回の Todo アプリの例では Container Component と Presentational Component に切り分けられているのでそこまであてはまらないかもしれないが

言ってることはわかる気がする。


Selector の導入

この問題を解決するのが reselect というライブラリである。

reselect は Selector という機能を提供する。

ざっくり言うと、Selector は state の中から自分が関心のあるツリー部分だけを抜き出してきて

抜き出してきたパラメータから必要な計算を行う。

先に、上の VisibleTodoList の例を Selector を用いて書き直すと、こうなる。


selectors/index.js

import { createSelector } from 'reselect';

const visibilityFilterSelector = (state) => state.visibilityFilter;
const todosSelector = (state) => state.todos;

export const visibleTodosSelector = createSelector(
[ visibilityFilterSelector, todosSelector ],
(visibilityFilter, todos) => {
switch (visibilityFilter) {
case 'SHOW_ALL':
return todos;
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed);
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed);
}
}
)



containers/VisibleTodoList.js

import { connect } from 'react-redux';

import { toggleTodo } from '../actions';
import TodoList from '../components/TodoList';
import { visibleTodosSelector } from '../selectors';

const mapStateToProps = (state) => {
return {
todos: visibleTodosSelector(state)
}
};


container component 側で計算していた処理が、 createSelector() なる関数にまるっと移行された。

ここで登場人物としては大きく2つある。


input selectors

これは上の例で言う visibilityFilterSelectortodosSelector が相当する。

input selector は「state を引数に受け取り、関心のある部分を返すだけの関数」で、後述する createSelector() の input になるためこう呼ばれる。

state から関心のある部分だけを抽出するのが役割なので、計算などは行わない。


createSelector(...inputSelectors | [inputSelectors], resultFunc)

こちらが reselect のメイン。

この関数は先ほどの input selectors を引数に受け取り、input selectors が返す結果を使った計算処理を関数として最後の引数に定義する。

上の例だと

(visibilityFilter, todos) => {

switch (visibilityFilter) {
case 'SHOW_ALL':
return todos;
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed);
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed);
}
}

部分が resultFunc

resultFunc の引数には、createSelector() に渡した input selectors の戻り値が順番に渡されてくる。


その他の API

https://github.com/reactjs/reselect#api を参照。

今回の例で登場しなかったので調べられてない。


createSelector() を使うと何がうれしいか

createSelector() の挙動は


createSelector determines if the value returned by an input-selector has changed between calls using reference equality (===).

...

Selectors created with createSelector have a cache size of 1. This means they always recalculate when the value of an input-selector changes, as a selector only stores the preceding value of each input-selector.


https://github.com/reactjs/reselect#createselectorinputselectors--inputselectors-resultfunc)

なので、呼ばれるたびに input selectors の戻り値(つまり state のうち関係する部分)に更新があったかどうかを === で比較し、更新がなかった場合は resultFunc は再実行せず、キャッシュしておいた直前の結果を利用する。

そのため、コンポーネント側からロジックが分離できただけでなく、state のツリーのうち、関係のある部分が更新されない限りは getVisibleTodos() による再計算は発生しない。

ということで、上述した問題が解決できた。ということなのだと理解した。


おまけ:createSelector()props も参照したい

参考:Accessing React Props in Selectors

mapStateToProps() 内で props も引数に渡してあげれば良い。


containers/VisibleTodoList.js

const mapStateToProps = (state, props) => {

return {
todos: visibleTodosSelector(state, props)
}
};

こうすると各 input selectors 内で props が使えるようになる。


selectors/index.js

const visibilityFilterSelector = (state, props) =>

state.todoLists[props.listId].visibilityFilter;
const todosSelector = (state, props) =>
state.todoLists[props.listId].todos;


おまけ:計算結果のメモ化(memoization)という考え方

公式ドキュメントを読んでいると memoized selector という単語が頻繁に登場する。

memoize(d) あるいは memoization は、日本語だと「メモ化」と呼び、Wikipedia では


メモ化(英: Memoization)とは、プログラムの高速化のための最適化技法の一種であり、サブルーチン呼び出しの結果を後で再利用するために保持し、そのサブルーチン(関数)の呼び出し毎の再計算を防ぐ手法である。


と書かれている。

今回 createSelector() がやっていることがまさにそれで、一度行った計算結果をメモ化して保存してくれるので、memoized selector という用語を使っているみたい。

input selectors は毎回実行されるので memoized ではなく、計算は行わないと言ったのはそういう理由から。


おわりに

元々感じていたのは「reducer を分割してるのに、ある action に対してすべての reducer が実行されるのはなんでなんだろう? action に対応する reducer だけ反応してくれればいいのに」という疑問だった。

今回の Selector によって、コンポーネント毎に自分が関心のある state だけを監視することができるようになったわけだけども

reducer は相変わらずすべて実行されているので、そこは今後も注意したい。

Redux はまだまだ理解できていないことだらけなので、間違ってるところがあればご指摘いただければ幸いです。