今回はhigher-order reducerについてです。色々な用途に使われますがSuPICEのstudioチームでは、 higher-order reducerを利用して作られているredux-undoを使用しています。 redux-undoはその名の通りundo/redoを実装するためのモジュールです。
1.higher-order reducerとは
higher-order reducerはreducerを頑張って日本語にすると高階reducerという形になるでしょうか。高階関数(higher-order function)が関数を引数としたり関数を戻り値とするように、 higher-order reducerはreducerを引数にしたり戻り値にする関数です(ソース1)。
ソース1. 実行時にfunctionの名称を出力する高階関数
function higerOrderFunction(func){ return function(){ console.log(func.name); return func(); }; }
2.実は使われているhigher-order reducer
小難しいことを書いているように感じるかもしれませんが、higer-order reducerは実はReduxを使っている大半の開発者は使用しています。 Reduxの関数であるcombineReducersは結合対象のreducerを引数に取り、結合したreducerを返すhiger-order reducerです。 higer-order reducerは聞いたことがなくてもcombineReducersはReduxを使用している大半の開発者は知っているのではないでしょうか。
ソース2. combineReducers
// combineReducersの使用例 var rootReducer = combineReducers({ reducerA: reducerA, reducerB: reducerB }); // エラー処理などを抜いて簡略化したcombineReducers function combineReducers(reducers){ var reducerKeys = Object.keys(reducers); // ① var finalReducers = {}; for (var i = 0; i < reducerKeys.length; i++){ var key = reducerKeys[i]; if (typeof reducers[key] === 'function'){ finalReducers[key] = reducers[key]; } } var finalReducerKeys = Object.keys(finalReducers); return function combination(state = {}, action){ // ② var hasChanged = false; var nextState = {}; for (var i = 0; i < finalReducerKeys.length; i++){ var key = finalReducerKeys[i]; var reducer = finalReducers[key]; var previousStateForKey = state[key]; var nextStateForKey = reducer(previousStateForKey, action); nextState[key] = nextStateForKey; hasChanged = hasChanged || nextStateForKey !== previousStateForKey; } return hasChanged ? nextState : state; } }
上のソースコードはredux内にあるcombineReducersからエラー処理や警告メッセージ表示処理を削除したものです。簡単に解説していきます。
function combineReducers(reducers){ var reducerKeys = Object.keys(reducers); // ① var finalReducers = {}; for (var i = 0; i < reducerKeys.length; i++){ var key = reducerKeys[i]; if (typeof reducers[key] === 'function'){ finalReducers[key] = reducers[key]; } } var finalReducerKeys = Object.keys(finalReducers);
①は前準備で引数からreducer以外のプロパティを引数から取り除き、同時にkeyを取得しています。特に解説するような点はないですね。
return function combination(state = {}, action){ // ② var hasChanged = false; var nextState = {}; for (var i = 0; i < finalReducerKeys.length; i++){ var key = finalReducerKeys[i]; var reducer = finalReducers[key]; var previousStateForKey = state[key]; var nextStateForKey = reducer(previousStateForKey, action); nextState[key] = nextStateForKey; hasChanged = hasChanged || nextStateForKey !== previousStateForKey; } return hasChanged ? nextState : state; } }
②で結合したreducerを返しています。返しているreducerの中身を掘り下げていくと、①で準備したreducerとkeyを利用して 各reducerにactionとstateを渡しstateの更新をしています。結合したreducerの内1つでもstateに変更があった場合、 nextStateを返すように実装されているというわけです。 (stateの比較が「!==」で行われているので同じobjectを返さないとnextStateが返りrenderが実行されてしまいますね。割と危険。)
3.higher-order reducerのメリット
さてこんなhigher-order reducerですが実際どんな役に立つのでしょうか。 わざわざreducer関連を複雑にしなくてもmiddlewareで解決すればよいような気もします。 筆者の個人の意見ですが、higher-order reducerのメリットは対象となるreducerを容易に限定できるということだと考えています。 対象となるreducerを限定できるということは、少し乱暴ですがstoreの一部だけを対象できるとも言えるのではないでしょうか。 Reduxは原則的にsingle storeですがstoreの一部だけを対象にしたい処理もあります。 例えばundo/redoなどのような処理です。 SuPICEを例にとって考えましょう。ReactのComponent内でstateを保持するケースもありますが、 viewのstateは基本的にReduxのstore内で保存されています。 つまりメニューの開閉やダイアログの表示/非表示、ユニット追加/削除もすべて同じstoreが管理しているわけです。 ユニットを誤って削除してしまったのでundoしようとしたら、さっき閉じたメニューが開いた。勘弁してもらいたいものです。 メニューの開閉のstateの変更は無視し、ユニットのstateの変更だけundo/redoの対象にしたいのです。 対象となるstoreの箇所を限定するhigher-order reducerの出番ではないでしょうか。
まとめ
正直な話をいうとredux-undoだけで全ての問題が解決するわけではありません。 「複数のactionを一括でundo/redoしたい」「一部のactionはundo/redoの対象にしたくない」など 実装をしていると別途解決しなければならない問題は山ほど出てきます。 undo/redoの条件を変更するためにreducerを切り替えるhigher-order reducerを用いたり、 そもそものstateの構成自体を変更したりと解決の手段は様々です。 今回は例としてundo/redoを上げましたが工夫次第で他にも色々とhigher-order reducerの活躍の機会はあると思われます。 なかなか興味深い技術なのではないでしょうか。