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 createLogger from "./logger";
import timestamp from "./timestamp";
// 4. Actionを受け取り新しいStateを返すReducer関数
const reducer = (state = {}, action) => {
switch (action.type) {
case "AddTodo":
return Object.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をdispatch
store.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で登録したコールバックを呼ぶ
via staltz.com/unidirectional-user-interface-architectures.html
次は middleware によりどのような拡張ができるのかを見ていきます。
middleware
Reduxでは第三者が拡張できる仕組みを middleware と呼んでいます。
どのような拡張を middleware で書けるのか、実際の例を見てみます。次の middleware はStoreがdispatchしたActionと、その前後でStateにどのような変更があったのかを出力するロガーです。
// LICENSE : MIT
const defaultOptions = {
// default: logger use console API
logger: console
};
/**
* create logger middleware
* @param {{logger: *}} options
* @returns {Function} middleware function
*/
export default function createLogger(options = defaultOptions) {
const logger = options.logger || defaultOptions.logger;
return store => 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
をもう一度見てみます。
export default function createLogger(options = defaultOptions) {
const logger = options.logger || defaultOptions.logger;
return store => 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 : MIT
const defaultOptions = {
// default: logger use console API
logger: console
};
/**
* create logger middleware
* @param {{logger: *}} options
* @returns {Function} middleware function
*/
export default function createLogger(options = defaultOptions) {
const logger = options.logger || defaultOptions.logger;
return store => 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");
export const ON_DISPATCH = "__ON_DISPATCH__";
/**
* The action object that must have `type` property.
* @typedef {Object} Action
* @property {String} type The event type to dispatch.
* @public
*/
export default class Dispatcher extends EventEmitter {
/**
* 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);
return this.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) => {
return middlewareAPI => {
const originalDispatch = (action) => {
middlewareAPI.dispatch(action);
};
// `api` is middlewareAPI
const wrapMiddleware = middlewares.map(middleware => {
return middleware(middlewareAPI);
});
// apply middleware order by first
const last = wrapMiddleware[wrapMiddleware.length - 1];
const rest = wrapMiddleware.slice(0, -1);
const roundDispatch = rest.reduceRight((oneMiddle, middleware) => {
return middleware(oneMiddle);
}, last);
return roundDispatch(originalDispatch);
};
};
export default applyMiddleware;
このapplyMiddleware
はReduxのものと同じなので、次のように middleware を適用した dispatch
関数を作成できます。
import Dispatcher from "./Dispatcher";
import applyMiddleware from "./apply-middleware";
import timestamp from "./timestamp";
import createLogger from "./logger";
const dispatcher = new Dispatcher();
dispatcher.subscribe(action => {
console.log(action);
/*
{ timeStamp: 1463742440479, type: 'FOO' }
*/
});
// Redux compatible middleware API
const state = {};
const middlewareAPI = {
getState(){
// shallow-copy state
return Object.assign({}, state);
},
dispatch(action){
dispatcher.dispatch(action);
}
};
// create `dispatch` function that wrapped with middleware
const 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 のみですべての処理が完結するわけではない