モチベーション
reduxでどのように非同期処理を扱うのか、redux界隈ではいくつか方法が考えられているが、それぞれの利点・弱点を調べてみる。
redux middlewareについて復習
redux middlewareのコンセプトは
It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.
とのこと。 redux middlewareはdispatching actionと、action objectがreducer関数に渡されるタイミングの間に存在する。
logging examples
dispatchの前後にconsole.logを使う
let action = addTodo('Use Redux')
console.log('dispatching', action)
store.dispatch(action)
console.log('next state', store.getState())
関数に切り出す
上記のconsole.log
とdispatch
をするところを関数に切り出す。
function dispatchAndLog(store, action) {
console.log('dispatching', action)
store.dispatch(action)
console.log('next state', store.getState())
}
dispatchAndLog(store, addTodo('Use Redux'))
ただ、これではstoreがパラメータになるので使いやすいとは言えない。
dispatch関数をmonkey patchingする
reduxのstore
object は、以下のいくつかの関数しかないシンプルなオブジェクト:
- getState()
- dispatch(action)
- subscribe(listener)
- replaceReducer(nextReducer)
なので、dispatch()
を monkey patching すると、store.dispatch()
はそのままに、自分の追加したい処理を実行できそう。(あまりいいやり方ではないにしても)
let next = store.dispatch
store.dispatch = function dispatchAndLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
複数の処理をmonkey patchingしたいときにはどうするか?
loggingとerror reportingの処理をmonkey patchingしたいときには以下のように書ける。
function patchStoreToAddLogging(store) {
let next = store.dispatch
store.dispatch = function dispatchAndLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
}
function patchStoreToAddCrashReporting(store) {
let next = store.dispatch
store.dispatch = function dispatchAndReportErrors(action) {
try {
return next(action)
} catch (err) {
console.error('Caught an exception!', err)
Raven.captureException(err, {
extra: {
action,
state: store.getState()
}
})
throw err
}
}
}
patchStoreToAddLogging(store)
patchStoreToAddCrashReporting(store)
monkey patchingを隠蔽する
monkey patchingな関数を「関数を返す関数」にして
function logger(store) {
let next = store.dispatch
// Previously:
// store.dispatch = function dispatchAndLog(action) {
return function dispatchAndLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
}
以下のように複数のmonkey patchingな関数を順に実行するような関数をつくることができる:
function applyMiddlewareByMonkeypatching(store, middlewares) {
middlewares = middlewares.slice()
middlewares.reverse()
// Transform dispatch function with each middleware.
middlewares.forEach(middleware =>
store.dispatch = middleware(store)
)
}
applyMiddlewareByMonkeypatching(store, [ logger, crashReporter ])
このmonkey patchingを、処理はそのままにライブラリのなかに隠蔽することができる。 次の項目で例を挙げる。
monkey patchingを取り除く
dispatch
関数をmonkey patchingする理由は、どこからでも共通の処理(monkey patchingしている関数)を呼び出せるようにするためである。
前の例で取り上げたapplyMiddlewareByMonkeypatching
のように、共通の処理をを順に行っていくやり方は以下のように書くこともできる:
const logger = store => next => action => {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
const crashReporter = store => next => action => {
try {
return next(action)
} catch (err) {
console.error('Caught an exception!', err)
Raven.captureException(err, {
extra: {
action,
state: store.getState()
}
})
throw err
}
}
これはredux middlewareのやり方と同様のもの。 この書き方では以下のような処理の流れになる:
- middleware関数は
next()
としてdispatch関数を受け取る next()
を次のmiddlewareに順に渡すdispatch関数を返す
store
のgetState()
などの関数を使えると便利なので、パラメータとしてstore
を渡せるようになっている。
middleware関数を適用するための案
前述したapplyMiddlewareByMonkeypatching()
のかわりに、以下のようなapplyMiddleware(store, middleware)
関数を書くことができる。
// Warning: Naïve implementation!
// That's *not* Redux API.
function applyMiddleware(store, middlewares) {
middlewares = middlewares.slice()
middlewares.reverse()
let dispatch = store.dispatch
middlewares.forEach(middleware =>
dispatch = middleware(store)(dispatch)
)
return Object.assign({}, store, { dispatch })
}
store => next => action => {}
の形式のmiddleware関数は、上記のようにdispatch関数をwrapすることができる。
applyMiddleware(store, middleware)
の最後で、middleware関数でwrapしたdispatch
関数をもつstoreを返す。
この方法は、以下の点で実際のredux middlewareのAPIと異なっている:
- redux middlewareにはstore APIの
dispatch(action)
とgetState()
しか使わせない - これは少しトリッキーだが、middleware関数内部で
next(action)
ではなくstore.dispatch(action)
を実行すると、actionはstore.dispatch()
したmiddlewareを含むすべてのmiddlewareの処理を再度順に実行することになる- Async Actions | Redux に例がある
- 1度だけmiddlewareが適用されるようにするには、
store
自体を使うのではなくcreateStore()
関数を使う(store, middlewares) => store
ではなく、(...middlewares) => (createStore) => createStore
を使う- これは
createStore
にパラメータとして渡して実行することができる(createStore | Redux)
最終案: reduxでどうmiddlewareを使うか
middleware関数は以下のようになる。
const logger = store => next => action => {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
const crashReporter = store => next => action => {
try {
return next(action)
} catch (err) {
console.error('Caught an exception!', err)
Raven.captureException(err, {
extra: {
action,
state: store.getState()
}
})
throw err
}
}
これをcreateStore
関数に、applymiddleware
関数を使ってenhancer
として渡す。
import { createStore, combineReducers, applyMiddleware } from 'redux'
let todoApp = combineReducers(reducers)
let store = createStore(
todoApp,
// applyMiddleware() tells createStore() how to handle middleware
applyMiddleware(logger, crashReporter)
)
applyMiddleware(…middlewares)関数は何をしているのか
middlewareのポイントは以下。
- 合成可能(composable)なこと
- middlewaresは、自分以外のmiddlewareが何をしているか知らなくてもよい
middlewaresのインターフェース
以下がmiddleware関数のインターフェースとなる。
({ getState, dispatch }) => next => action
thunk middlewareの例
かなり簡単なmiddlewareだったので引用する。
dispatch()
のパラメータとしてaction objectではなく関数を受け取る- 受け取った関数をreducer関数が実行される前に実行
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
redux Thunk
- redux Thunk middlewareは、dispatchするときのパラメータをactionオブジェクトだけではなく、関数をも受け取れるようにする
- このdispatchするときに渡す関数は、
dispatch
とgetState
関数を引数として受け取れる
- このdispatchするときに渡す関数は、
サンプルコードをそのまま見ていくとわかりやすい。
componentではdispatch(actionCreator())
を実行する。
actionCreator
は、dispatch
とgetState
関数を引数に取る関数を返す。redux-thunk middlewareはこのdispatchの使い方を許容する。
return (dispatch, getState) => {
fetchPromise.then((res) => {
dispatch(actionCreatoer(res));
});
}
この関数の中で、fetchなどの非同期処理を行うPromiseのthenを使って、任意のタイミングでViewに状態を反映させるためのactionをactionCreator関数で作成し、actionオブジェクトをdispatchする。
thunkとは
Thunk - Wikipedia, the free encyclopedia
現時点(2016/07/24)では英語版, ドイツ語版, スペイン語版しかページがないため、わりと新しい用語?なのだと思われる。 thunkとはsubroutineのサポートをするために、(殆どの場合は)自動的につくられるsubroutineのことだそう。
下記の例のuse関数のようなものがthunk関数の例、とのこと。
class A {
int value;
virtual int access() { return this->value; }
};
class B {
int value;
virtual int access() { return this->value; }
};
class C : public A, public B {
int better_value;
virtual int access() { return this->better_value; }
};
int use(B *b) {
return b->access();
}
// ...
B someB;
use(&someB);
C someC;
use(&someC);
redux-saga…の前に、redux-thunkの問題点
yelouafi/redux-saga: An alternative side effect model for Redux apps
You might’ve used redux-thunk before to handle your data fetching. Contrary to redux thunk, you don’t end up in callback hell, you can test your asynchronous flows easily and your actions stay pure.
redux-sagaのREADMEによると、redux-thunkには以下の2点の問題があるという:
- callback hell
- あくまで予想だが、複数のfetchを行ったり、複雑なロジックが絡んでくるとわかりにくくなるということなのだと思う
- テストが難しい
- action creatorがaction objectを返す役割ではなくなる?(
your actions stay pure
のところ)
callback hell
テストが難しい
たしかに下記のようなthunk関数をテストするときに、Promiseをスタブにしないといけないので面倒だとは思った。
return (dispatch, getState) => {
fetchPromise.then((res) => {
dispatch(actionCreatoer(res));
});
}
action creatorがaction objectを返す役割ではなくなる
非同期処理をreducerに入れるか、action cratorに入れるか?という論点がある。 私はは非同期処理はaction creatorに入れたほうがよいと思う。 そのため、redux-sagaのREADMEが言っているようなaction creatorがpure action creatorではないことに特に問題を感じなかった。
理由は、reducerはViewのための (curentState) => newState
にしたほうがすっきりする気がするため。
画面表示に用いるためのデータの構造と、fetchするときにもらってくるデータの構造は異なっている場合がある。
reducerは画面に表示させるためのデータ(=reduxのstate)の生成に集中したほうがわかりやすい。