Redux
ReduxはJavaScriptアプリケーションのStateを管理するライブラリで、Reactなどと組み合わせアプリケーションを作成するために利用されています。
ReduxはFluxアーキテクチャに類似する仕組みです。そのため、事前にFluxについて学習しているとよいです。
ReduxにはThree Principles(以下、三原則)と呼ばれる3つの制約の上で成立しています。
- Single source of truth
- アプリケーション全体のStateは1つのStateツリーとして保存される
- State is read-only
- StateはActionを経由しないと書き換えることができない
- Changes are made with pure functions
- Actionを受け取りStateを書き換えるReducerと呼ばれるpure functionを作る
この三原則についての詳細はドキュメントなどを参照してください。
Reduxの使い方についてはここでは解説しませんが、Reduxの拡張機能となるmiddleware も、この三原則に基づいた仕組みとなっています。
middleware という名前からも分かるように、Connectの仕組みと類似点があります。Connectの違いを意識しながら、Reduxのmiddleware の仕組みを見ていきましょう。
どう書ける?
簡潔にReduxの仕組みを書くと次のようになります。
- 操作を表現するオブジェクトをActionと呼ぶ
- 一般的なコマンドパターンのコマンドと同様のもの
- Actionを受け取りStateを書き換える関数をReducer と呼ぶ
- ReducerはStoreへ事前に登録する
- ActionをDispatch(
store.dispatch(action)
)することで、ActionをReducerへ通知する
Reduxの例として次のようなコードを見てみます。
import {createStore, applyMiddleware}from"redux";import createLoggerfrom"./logger";import timestampfrom"./timestamp";// 4. Actionを受け取り新しいStateを返すReducer関数const reducer =(state = {}, action) => {switch (action.type) {case"AddTodo":returnObject.assign({}, state, {title: action.title});default:return state; }};// 1. `logger`と`crashReporter`のmiddlewareを適用した`createStore`関数を作るconst createStoreWithMiddleware = applyMiddleware(createLogger(), timestamp)(createStore);// 2. Reducerを登録したStoreを作成const store = createStoreWithMiddleware(reducer);store.subscribe(() => {// 5. Stateが変更されたら呼ばれるconst state = store.getState();// 現在のStateを取得console.log(state);});// 3. Storeの変更をするActionをdispatchstore.dispatch({type:"AddTodo",title:"Todo title"});
logger
とcrashReporter
のmiddlewareを適用したcreateStore
関数を作る- Reducerを登録したStoreを作成
- (Storeの変更をする)Actionをdispatch
- Actionを受け取り新しいStateを返すReducer関数
- Stateが変更されたら呼ばれる
というような流れで動作します。
上記の処理のうち、 3から4の間がmiddleware が処理する場所となっています。
dispatch(action) -> (_middleware_ の処理) -> reducerにより新しいStateの作成 -> (Stateが変わったら) -> subscribeで登録したコールバックを呼ぶ
viastaltz.com/unidirectional-user-interface-architectures.html
次はmiddleware によりどのような拡張ができるのかを見ていきます。
middleware
Reduxでは第三者が拡張できる仕組みをmiddleware と呼んでいます。
どのような拡張をmiddleware で書けるのか、実際の例を見てみます。次のmiddleware はStoreがdispatchしたActionと、その前後でStateにどのような変更があったのかを出力するロガーです。
// LICENSE : MITconst defaultOptions = {// default: logger use console API logger:console};/** * create logger middleware * @param {{logger: *}} options * @returns {Function} middleware function */exportdefaultfunctioncreateLogger(options = defaultOptions){const logger = options.logger || defaultOptions.logger;returnstore => next =>action => { logger.log(action);const value = next(action); logger.log(store.getState());return value; };}
このmiddleware は次のようにReduxに対して適用できます。
import {createStore, applyMiddleware}from"redux";const createStoreWithMiddleware = applyMiddleware(createLogger())(createStore);
このとき、見た目上はstore
に対してmiddleware が適用されているように見えますが、実際にはstore.dispatch
に対して適用され、拡張されたdispatch
メソッドが作成されています。
これにより、dispatch
を実行する際にmiddleware の処理を挟むことができます。これがReduxのmiddleware による拡張ポイントになっています。
store.dispatch({type:"AddTodo",title:"Todo title"});
先ほどのlogger.js
をもう一度見てみます。
exportdefaultfunctioncreateLogger(options = defaultOptions){const logger = options.logger || defaultOptions.logger;returnstore => next =>action => { logger.log(action);const value = next(action); logger.log(store.getState());return value; };}
createLogger
は、loggerにオプションを渡すためのものなので置いておき、return
している高階関数の連なりがmiddleware の本体となります。
const middleware =store => next =>action => {};
上記のArrowFunctionの連なりが一見すると何をしているのかが分かりにくいですが、これは次のように展開できます。
const middleware =(store) => {return(next) => {return(action) => {// Middlewareの処理 }; };};
ただ単に関数を返す関数(高階関数)を作っているだけだと分かります。
これを踏まえてlogger.js
をもう一度見てみると、next(action)
の前後にログ表示を挟んでいることが分かります。
// LICENSE : MITconst defaultOptions = {// default: logger use console API logger:console};/** * create logger middleware * @param {{logger: *}} options * @returns {Function} middleware function */exportdefaultfunctioncreateLogger(options = defaultOptions){const logger = options.logger || defaultOptions.logger;returnstore => next =>action => { logger.log(action);const value = next(action); logger.log(store.getState());return value; };}
このmiddleware は次のようなイメージで動作します。
この場合のnext
はdispatch
と言い換えても問題ありませんが、複数のmiddleware を適用した場合は、次のmiddleware を呼び出すということを表現しています。
Reduxのmiddleware の仕組みは単純ですが、見慣れないデザインなので複雑に見えます。実際に同じ仕組みを実装しながら、Reduxのmiddleware について学んでいきましょう。
どのような仕組み?
middleware はdispatch
をラップする処理ですが、そもそもdispatch
とはどういうことをしているのでしょうか?
簡潔に書くと、Reduxのstore.dispatch(action)
はstore.subscribe(callback)
で登録したcallback
にaction
を渡し呼び出すだけです。
これはよくみるPub/Subのパターンですが、今回はこのPub/Subパターンの実装からみていきましょう。
Dispatcher
ESLintと同様でEventEmitterを使い、dispatch
とsubscribe
をもつDispatcher
を実装すると次のようになります。
const EventEmitter =require("events");exportconst ON_DISPATCH ="__ON_DISPATCH__";/** * The action object that must have `type` property. * @typedef {Object} Action * @property {String} type The event type to dispatch. * @public */exportdefaultclassDispatcherextendsEventEmitter{/** * subscribe `dispatch` and call handler. it return release function * @param {function(Action)} actionHandler * @returns {Function} call the function and release handler */ subscribe(actionHandler) {this.on(ON_DISPATCH, actionHandler);returnthis.removeListener.bind(this, ON_DISPATCH, actionHandler); }/** * dispatch action object. * @param {Action} action */ dispatch(action) {this.emit(ON_DISPATCH, action); }}
Dispatcher
はActionオブジェクトをdispatch
すると、subscribe
で登録されていたコールバック関数を呼び出すという単純なものです。
また、このDispatcher
の実装はReduxのものとは異なるので、あくまで理解のための参考実装です。
Unlike Flux, Redux does not have the concept of a Dispatcher.This is because it relies on pure functions instead of event emitters--Prior Art | Redux
applyMiddleware
次に、middleware を適用する処理となるapplyMiddleware
を実装していきます。先ほども書いたように、middleware はdispatch
を拡張する仕組みです。
applyMiddleware
はdispatch
とmiddleware を受け取り、middleware で拡張したdispatch
を返す関数です。
/* => api - middleware api => next - next/dispatch function => action - action object */const applyMiddleware =(...middlewares) => {returnmiddlewareAPI => {const originalDispatch =(action) => { middlewareAPI.dispatch(action); };// `api` is middlewareAPIconst wrapMiddleware = middlewares.map(middleware => {return middleware(middlewareAPI); });// apply middleware order by firstconst last = wrapMiddleware[wrapMiddleware.length -1];const rest = wrapMiddleware.slice(0,-1);const roundDispatch = rest.reduceRight((oneMiddle, middleware) => {return middleware(oneMiddle); }, last);return roundDispatch(originalDispatch); };};exportdefault applyMiddleware;
このapplyMiddleware
はReduxのものと同じなので、次のようにmiddleware を適用したdispatch
関数を作成できます。
import Dispatcherfrom"./Dispatcher";import applyMiddlewarefrom"./apply-middleware";import timestampfrom"./timestamp";import createLoggerfrom"./logger";const dispatcher =new Dispatcher();dispatcher.subscribe(action => {console.log(action);/* { timeStamp: 1463742440479, type: 'FOO' } */});// Redux compatible middleware APIconst state = {};const middlewareAPI = { getState(){// shallow-copy statereturnObject.assign({}, state); }, dispatch(action){ dispatcher.dispatch(action); }};// create `dispatch` function that wrapped with middlewareconst dispatchWithMiddleware = applyMiddleware(createLogger(), timestamp)(middlewareAPI);dispatchWithMiddleware({type:"FOO"});
applyMiddleware
でtimestamp
をActionに付加するmiddleware を適用しています。これによりdispatchWithMiddleware(action)
したaction
には自動的にtimestamp
プロパティが追加されています。
const dispatchWithMiddleware = applyMiddleware(createLogger(), timestamp)(middlewareAPI);dispatchWithMiddleware({type:"FOO"});
ここでmiddleware にはmiddlewareAPI
として定義した2つのメソッドをもつオブジェクトが渡されています。しかし、getState
は読み込みのみで、middlewareにはStateを直接書き換える手段が用意されていません。また、もう1つのdispatch
もActionオブジェクトを書き換えられますが、結局できることはdispatch
するだけです。
このことからmiddleware にも三原則が適用されていることが分かります。
- State is read-only
- StateはActionを経由しないと書き換えることができない
middleware という仕組み自体はConnectと似ています。しかし、middleware が結果(State)を直接書き換えることはできません。
Connectのmiddleware は最終的な結果(response
)を書き換えできます。一方、Reduxのmiddleware は扱える範囲が「dispatch
からReducerまで」と線引されている違いといえます。
どういうことに向いている?
Reduxのmiddleware そのものも三原則に基づいた仕組みとなっています。middleware はActionオブジェクトを自由に書き換えたり、Actionを無視したりできます。一方、Stateを直接は書き換えることができません。
多くのプラグインの仕組みでは、プラグインに特権的な機能を与えていることが多いですが、Reduxのmiddleware は書き込みのような特権的な要素も制限されています。
middleware に与えられている特権的なAPIとしては、getState()
とdispatch()
ですが、どちらも書き込みをするようなAPIではありません。
このように、プラグインに対して一定の権限をもつAPIを与えつつ、原則を壊すような特権を与えないことを目的としている場合に向いています。
どういうことに向いていない?
一方、プラグインにも書き込み権限を与えないためには、プラグイン間でやり取りする中間的なデータが必要になります。
ReduxではActionオブジェクトというような命令(コマンド)を表現したオブジェクトに対して、Reducerという命令を元に新しいStateを作り出す仕組みを設けていました。
つまり、プラグインそのものだけですべての処理が完結するわけではありません。プラグインで処理した結果を受け取り、その結果を処理する実装も同時に必要となっています。Reduxではmiddleware を前提とした処理を実装として書くことも多いです。
そういう意味ではプラグインと実装が密接といえるかもしれません。
そのため、プラグインのみで全処理が完結するような機能を作る仕組みは向いていません。
まとめ
ここではReduxのプラグインアーキテクチャについて学びました。
- Reduxのmiddleware はActionオブジェクトに対する処理を書ける
- middleware に対しても三原則が適用されている
- middleware に対しても扱える機能の制限を適用しやすい
- middleware のみですべての処理が完結するわけではない