Middleware使ってますか? 以前 Reduxのmiddlewareを積極的に使っていく という記事でMiddlewareの使いどころについて具体的な利用例を挙げました。本稿ではMiddlewareを書く上で役に立つ、もうちょっと掘り下げたTipsを紹介していきます。
ActionがMiddlewareを通り抜ける順番
export const m1 = store => next => action => {
console.log('m1', action.type);
return next(action);
};
export const m2 = store => next => action => {
console.log('m2', action.type);
return next(action);
};
export const m3 = store => next => action => {
console.log('m3', action.type);
return next(action);
};
import { createStore, applyMiddleware } from 'redux';
import reducer from './reducers';
import { m1, m2, m3 } from './middlewares';
export default createStore(
reducer,
applyMiddleware(m1, m2, m3)
);
抜粋ですが上記の構成で何か適当なAction、ここでは HOGE
Actionをdispatchすると以下の出力を得ます。
m1 HOGE
m2 HOGE
m3 HOGE
素直に applyMiddleware
で指定した順番にActionが通り抜けています。
強いて言えばMiddlewareによってはどこに置くかが大事なものがあるのでそこは注意すべきです。
例えば redux-logger
は README.md
にも書いてありますが 原則的に一番最後 に置きます。
import logger from 'redux-logger';
export default createStore(
reducer,
applyMiddleware(your, awesome, middleware, logger())
);
redux-logger
自体はただのロガーなのでどこに置いても動作しますが、最後に置くのはログ出力されるActionと実際にStoreに送り込まれる(その結果としてStateを変化させる)Actionに食い違いが出て混乱するからです。以下のようにわざと途中に redux-logger
を置いたMiddleware構成を想定してみます。
applyMiddleware(m1, logger(), m2, m3)
ここで m2
が FOO
Actionを潰して、m3
が別の BAR
Actionを生成する場合、FOO
Actionと HELLO
Actionを送り込むと redux-logger
のログ出力には FOO
Actionと HELLO
Actionが表示されますが、実際にStoreに到達するActionは HELLO
Actionと BAR
Actionになります。
[M1] -> [Logger] -> [M2] -> [M3] -> [Store]
[FOO] --------------> X
[HELLO] ------------------------------->
[BAR] ----->
同様にMiddlewareを置く位置が大事なものとして redux-thunk
があります。redux-thunk
はActionオブジェクトの代わりに関数オブジェクトをdispatchすると、Storeの dispatch
関数と状態取得のための getState
関数を渡されるのであとは煮るなり焼くなりしてくれ、という良く言えば 強力な、悪く言えば GOTO文のような Middlewareです。こちらは redux-logger
と逆で 一番最初に置く 必要があります。
import thunk from 'redux-thunk';
export default createStore(
reducer,
applyMiddleware(thunk, your, awesome, middleware)
);
理由は単純で、多くのMiddlewareはActionとして関数オブジェクトがやって来ることは想定していないので、すべてのMiddlewareに先んじて関数を実行してActionオブジェクトを得てから後続のMiddlewareに流してあげる必要があるためです。
[Thunk] -> [M1] -> [M2] -> [Store]
[Func] --> X ----> [A1] -------------->
redux-api-middleware
とかも同じで最初に置く必要があります。
では redux-thunk
と redux-api-middleware
はどっちを先にするべきでしょうか?
答えは 基本的にはどっちでもいい です。redux-thunk
は redux-api-middlware
の CALL_API
Actionを無視してくれますし、redux-api-middlware
は redux-thunk
で処理される関数オブエジェクトを無視するからです。
redux-api-middleware
の types
に関数オブジェクトを渡して redux-thunk
に実行させるような 変態的 特殊なことをやりたくても、現状では redux-api-middleware
のバリデーションによってはじかれるのでできません。残念。もし将来 types
に関数オブジェクトを渡せるようになったら redux-api-middleware
を先に置く 必要があります。逆にすると関数オブジェクトは実行されずに後続のMiddlewareまたはStoreでエラーが発生します。これは redux-api-middleware
がAPI呼び出しの状況や結果を伝えるActionを投げるときに store.dispatch
を使わずに next
を使っている ことに起因します。次のセクションではこの違いが何を意味しているのか考えていきます。
MiddlewareからActionを投げる方法は2つある
Middlewareはだいたい以下のような形式で書きます。
export const hoge = store => next => action => {
// ...
};
Middlewareによっては一番外側の関数をarrow functionではなく普通に関数として定義してたり、引数を受け取るとき (store)
の代わりに ({ getState })
と書いてStoreに対しては状態の取得しかしないよ!と明示するものもありますね。
さて、Middlewareでは新しくActionオブジェクトを生成してStoreにdispatchすることができますが、その方法は2つあることはご存知でしょうか? 例えば FOO
Actionが来たら BAR
Actionを新しく作って投げるMiddlewareを考えてみます。
export const m1 = store => next => action => {
if (action.type === 'FOO') {
next({ type: 'BAR' });
}
return next(action);
};
特別なことはありません。それではもう1つの方法を以下に示します。
export const m2 = store => next => action => {
if (action.type === 'FOO') {
store.dispatch({ type: 'BAR' });
}
return next(action);
};
このようにStoreの dispatch
関数を直接呼び出すこともできます。言われてみるとなんてことはないんですけどね・・・。
では next
と store.dispatch
の違いは異なる結果を生み出すのでしょうか?
Middlewareが1つだけのときに限って NO ですが、 基本的にはYES なので注意が必要です。具体的にどういう違いがあるのか詳しく見ていきます。次のコードは冒頭の3つのMiddlewareのサンプルをベースに、真ん中の m2
の代わりに next
を使う m2a
、store.dispatch
を使う m2b
に書き換えました。動作としてはどちらも FOO
Actionが来たら BAR
Actionを生成して投げます。
export const m1 = store => next => action => {
console.log('m1', action.type);
return next(action);
};
export const m2a = store => next => action => {
console.log('m2a', action.type);
if (action.type === 'FOO') {
next({ type: 'BAR' });
}
return next(action);
};
export const m2b = store => next => action => {
console.log('m2b', action.type);
if (action.type === 'FOO') {
store.dispatch({ type: 'BAR' });
}
return next(action);
};
export const m3 = store => next => action => {
console.log('m3', action.type);
return next(action);
};
上記Middlewareを使ってまずは m2a
を使った場合、applyMiddleware(m1, m2a, m3)
の出力を見てみます。
m1 FOO
m2 FOO
m3 BAR
m3 FOO
次に m2b
を使った場合、applyMiddleware(m1, m2b, m3)
の出力です。
m1 FOO
m2 FOO
m1 BAR
m2 BAR
m3 BAR
m3 FOO
これで違いは明らかですね。 next
は後続のMiddlewareのみ通過するのに対し、store.dispatch
はMiddlewareチェーンの最初から実行されます。 どちらを使うべきかは何をしたいのかに依存します。例えば redux-thunk
では store.dispatch
を使うことを強制されますが、もし next
を使えてしまうと redux-thunk
をMiddlewareチェーンのどこに置いたかによって適用されるMiddlewareが変わってしまい混乱の元になります。さらに store.dispatch
を使うことによって関数オブジェクトがさらに関数オブジェクトをdispatchするような状況でも再度 redux-thunk
が実行してくれます。このように redux-thunk
では store.dispatch
を使わなければならない強い理由があります。
一方で新しいActionを投げるのに絶対に next
を使わなければならない状況というのはそんなに多くありません。 **だからといって安易にstore.dispatch
を使うのは危険です。**注意して作らないとMiddlewareがActionをdispatchすることで再帰的にMiddleware自身が呼び出されてしまう 無限ループ に陥ります。
例えば上記のサンプルでは FOO
Actionのときのみ BAR
Actionをdispatchしていますが、その条件をはずすと当然無限ループになります。redux-thunk
も関数オブジェクトであれば問答無用で実行するので、次のような意地悪なActionを投げると Uncaught RangeError: Maximum call stack size exceeded
エラーが発生します。
const recurring = dispatch => dispatch(recurring);
this.props.dispatch(recurring);
MiddlewareからActionを投げるタイミングは2つある
Middleware内からActionを投げるのに2つの方法があることを知りましたが、まだ安心できません。 どのタイミングで投げるのか によって結果が変わってきます。以下のMiddlewareがどのような挙動になるのか想像してみてください。
function isTarget(action) {
return action.type === 'FOO';
}
export const m1 = store => next => action => {
if (isTarget(action)) {
store.dispatch({ type: 'BAR1' });
}
const ret = next(action);
if (isTarget(action)) {
store.dispatch({ type: 'BAR2' });
}
return ret;
};
オリジナルのActionを next
で送り出す 前に 新しいActionをdispatchするのか、 後に dispatchするのかで変わってきます。これをうまく利用しているのが redux-logger
です。redux-logger
はStoreにdispatchされたActionを表示するだけでなく、dispatch前後のStateも出力してくれます。Storeの getState
関数を next
の前後でそれぞれ呼び出すことで実現しています。
まとめ
3行でまとめると、
-
applyMiddleware
で指定するMiddlewareの位置に気をつける - Actionを投げる方法は
next
とstore.dispatch
の2つがある - オリジナルのActionを
next
で呼び出す前後でStateが変化する
おまけ
複雑なMiddlewareチェーンを組んでいるとどこでActionが生成されたのか、改変されたのか、潰されたのかわからなくなることがあります(怖い)。そんなとき手っ取り早くprintデバッグする方法は redux-logger
をひたすらMiddlewareの間に差し込みます。
import logger from 'redux-logger';
export default createStore(
reducer,
applyMiddleware(logger(), your, logger(), awesome, logger(), middleware, logger())
);
うわぁひどい・・・。けど、 惨状 状況は一応つかめます。ただこれもMiddlewareの数が多くなると挿入するのが面倒になるので、お手軽にやるには redux-middleware-logger が便利です。使い方は簡単でMiddlewareのリストを渡します。
import withLogger from 'redux-middleware-logger';
export default createStore(
reducer,
applyMiddleware(...withLogger(your, awesome, middleware))
);
あまりインストールされていませんね。みなさんそんなに困っていないようで安心しました。