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

  • 101
    いいね
  • 2
    コメント
この記事は最終更新日から1年以上が経過しています。

2015-08-28 11:06 合成された初期状態ツリーの出力結果を追加

分割されたreducerの初期状態ツリー

Reduxの原則の1つであるグローバルな状態ツリーがすべてのソースとなるという点。
理屈ではわかるんだけど、いくつかコンポーネントを作っていくと常に大きなツリーが渡されるってのがとても扱いづらく感じる。つまり、何かアクションを受け取ってそれを状態ツリーに反映させるとき、ほとんどのケースにおいてツリーの一部だけを更新して、それ以外はいじらないことが多いからだ。

Reduxリポジトリのasyncのサンプルコードを追っていくと、reducers/index.jsの部分がよくわからなかった。
他のサンプルだと初期状態ツリーは定数として定義してreducerのデフォルト引数に指定していることが多かったが、asyncではまずそれがない。
そしてなにより状態ツリーはグローバルなものが1つで、それがすべてであるという説明を読んでいたので、postsByRedditは文字列、selectedRedditはオブジェクトというように初期状態の構造が異なっていることには混乱した。

そうなると怪しいのはcombineReducersになる。最初に名前を見た時、単に1つずつ実行しているだけだろうと思ったけど実際にはそれ以外にもやっていた。そしてそれが各reducerにおいて初期状態の構造が異なっている理由だった。

combineReducersがやっていること

combineReducersの処理の本筋部分だけを取り出すと

  • 各reducerを呼び出して初期状態を取り出す
  • 初期状態をまとめて初期状態ツリーを作る
  • reducerの処理をまとめたcombination関数を返す

となっていて、combineReducersが初期状態ツリーの生成も担っていた。

さらにcombination関数の処理について

combineReducersが返すcombination関数はreducer名をキーにした状態ツリーから、そのreducerの部分状態を取り出して渡す。戻ってきた新しい部分状態はやはりreducer名をキーにしてまた1つの状態ツリーにまとめられて、最終的なreducerの戻り値となる。

function reducerA(state = 'hoge', action) {
  switch (action.type) {
    case 'ACTION_X':
      return 'foobar';
  }
  return state;
}

function reducerB(state = { flag: false, items: [] }, action) {
  switch (action.type) {
    case 'ACTION_Y':
      return Object.assign({}, state, { flag: true });
  }
  return state;
}

let rootReducer = combineReducers({ reducerA, reducerB });

// stateとしてundefinedを渡して初期状態ツリーを取り出す
console.log(rootReducer(undefined, { type: null }));

実行すると以下の初期状態ツリーが出てくる。

{
  "reducerA": "hoge",
  "reducerB": {
    "flag": false,
    "items": []
  }
}

reducerをどう分割していくか

combineReducersを使うことでreducerごとに別々の名前空間で処理が実行されているようなものなので(chrootみたいな感じ)、reducerの分割単位は状態ツリーのどの部分ツリーをスコープとするかがポイントになるようだ。

基本的には個々のreducerは最小限の部分ツリーで動くように作って、どうしてもツリーを横断的に処理する必要があるものについてはそれらを含むちょっと大きめのツリーを渡すreducerに任せる、って感じで作っていくといいのかな。

  • この記事は以下の記事からリンクされています
  • Reduxのreselectとはからリンク