Redux の機能を拡張し、非同期処理などを書きやすくする仕組みである Redux Middleware について書いていく。
この記事で使っているライブラリのバージョンは以下。
- redux@3.7.2
- react-redux@5.0.7
- react@16.2.0
- react-dom@16.2.0
Redux の基本については、以前書いた。
numb86-tech.hatenablog.com
最近読んだこの記事も分かりやすかった。
Vanilla JSで学ぶRedux - Qiita
reducer はプレーンなオブジェクトのみを受け付ける
Middleware の役割を理解するためにはまず、 Redux の仕組みを理解しないといけない。
dispatch
を使ってaction
をreducer
に渡し、そのreducer
が渡されたaction
の内容に応じて新しくstate
を生成し、それをstore
が記録する。
というのが、Reduxの基本的な流れである。
そしてaction
は、type
プロパティを持ったプレーンなオブジェクトでなければならない。
ここでいう「プレーンなオブジェクト」は、Redux内部のisPlainObject
という関数で判定している。
ここで問題になるのが、promiseオブジェクト。
isPlainObject
でエラーになる。
そのため、APIなどの非同期処理の結果をreducer
に渡そうとすると、エラーになってしまう。
import {createStore} from 'redux';
const ADD_COUNTER = 'ADD_COUNTER';
const addCounter = value =>
Promise.resolve(value).then(res => ({type: ADD_COUNTER, value: res}));
const myReducer = (state = 0, action) => {
const {type, value} = action;
switch (type) {
case ADD_COUNTER:
return state + value;
default:
return state;
}
};
const myStore = createStore(myReducer);
const action = addCounter(1);
myStore.dispatch(action);
このエラー処理はdispatch
の内部で行っており、reducer
に渡す前にエラーになっている。
そのため例えば、dispatch
を拡張してpromiseオブジェクトをハンドリングしてあげれば、非同期処理の結果をreducer
に渡すことが出来る。
dispatch の拡張
以下は、dispatch
を上書きして、本来のdispatch
の処理の前に非同期処理の結果をハンドリングしている。
こうすることで、reducer
にはプレーンなオブジェクトが渡されるようになる。
const originalDispatch = myStore.dispatch;
myStore.dispatch = action => {
action.then(res => {
console.log(res);
originalDispatch(res);
console.log(myStore.getState());
});
};
const action = addCounter(1);
myStore.dispatch(action);
だが既存のdispatch
を上書きしてしまうこの方法は、もちろん望ましくない。
Reduxには、dispatch
を拡張するための方法が予め用意されている。
それが Middleware である。
Middleware を使うことで、dispatch
の機能を拡張してreducer
の前後に任意の処理を追加できるようになる。
applyMiddleware
Middleware を使うには、createStore
の第二引数でapplyMiddleware
を呼び出す。
import {createStore, applyMiddleware} from 'redux';
const myStore = createStore(myReducer, applyMiddleware(myMiddleware));
applyMiddleware
の引数に、適用する Middleware を入れる。
後で改めて説明するが、applyMiddleware
を使用すると、dispatch
が実行された際に、dispatch
ではなく指定した Middleware が呼び出されるようになる。
Middleware は複数指定することが出来るが、まずは理解しやすくするために一つだけ使ってみる。
先程の非同期処理の例を解決するには、次のように書けばよい。
import {createStore, applyMiddleware} from 'redux';
const ADD_COUNTER = 'ADD_COUNTER';
const addCounter = value =>
Promise.resolve(value).then(res => ({type: ADD_COUNTER, value: res}));
const myReducer = (state = 0, action) => {
const {type, value} = action;
switch (type) {
case ADD_COUNTER:
return state + value;
default:
return state;
}
};
const myMiddleware = store => next => action => {
action.then(res => {
console.log(res);
next(res);
console.log(store.getState());
});
};
const myStore = createStore(myReducer, applyMiddleware(myMiddleware));
const action = addCounter(1);
myStore.dispatch(action);
非同期処理のハンドリングを、myMiddleware
で行っている。
次は、このmyMiddleware
を例にして、Middleware がどのように動いているのかを見ていく。
Middleware の構造と引数について
Middleware でまず目を引くのは、store => next => action =>
という連続したアロー関数。
最初に見た時は戸惑ったが、これは関数を返す関数、という構造が入れ子になっているだけで、複雑なものではない。
例えば先程のmyMiddleware
は、以下のように書くことも出来る。
function myMiddleware(store) {
return function(next) {
return function(action) {
action.then(res => {
console.log(res);
next(res);
console.log(store.getState());
});
};
};
}
先述のように、 Middleware はdispatch
したときに呼ばれる。
つまりmyStore.dispatch(action)
としたときに、以下が実行される。
myMiddleware(store)(next)(action)
もちろんこれは単純化したもので、実際にはこんなシンプルな実装ではないだろうが。
次に、Middleware に渡される3つの引数について見ていく。
const myMiddleware = store => next => action => {
console.log(store);
console.log(next);
console.log(action);
};
store
はcreateStore
で作ったstore
だが、それそのものではなく、持っているメソッドはgetState
とdispatch
のみ。
next
は、Middleware を適用する前の、本来のdispatch
。Middleware を複数使う時は必ずしもそうはならないのだが、 Middleware が一つのときは必ずdispatch
になる。
action
は、myStore.dispatch(action)
でdispatch
に渡されたaction
。
next
とaction
が重要で、next(action)
とすると、本来のdispatch(action)
を呼び出すことになる。
つまり、action
をreducer
に渡す。
これにより、reducer
の前後に任意の処理を追加して、promiseオブジェクトをハンドリングしたり、reducer
によって更新されたあとのstate
を取得したりすることが出来るのである。
const myMiddleware = store => next => action => {
action.then(res => {
next(res);
console.log(store.getState());
});
};
Middleware のなかでnext
を呼ばない、ということも可能ではある。
その場合、dispatch
せず、つまりreducer
を実行せずに、処理が終わる。
const myMiddleware = store => next => action => {
console.log('myMiddleware は実行されるが next は呼ばない');
};
const myStore = createStore(myReducer, applyMiddleware(myMiddleware));
myStore.dispatch({type: ADD_COUNTER, value: 1});
console.log(myStore.getState());
Middleware のチェーン
ここまでは、 Middleware が一つだけのパターンを見てきた。
では、applyMiddleware(middleware1, middleware2)
のように複数の Middleware を使うとどうなるのか。
これが Middleware の大きな特徴の一つだと思うが、 Middleware の処理をチェーンのように連続でつないでいくことが可能になる。
これも先に例を示す。
addOne
とdouble
という2つの Middleware を使っている。
それによるstate
の変化も載せておく。
import {createStore, applyMiddleware} from 'redux';
const ADD_COUNTER = 'ADD_COUNTER';
const myReducer = (state = 0, action) => {
const {type, value} = action;
switch (type) {
case ADD_COUNTER:
return state + value;
default:
return state;
}
};
const addOne = store => next => action => {
next(action + 1);
};
const double = store => next => action => {
next({type: ADD_COUNTER, value: action * 2});
};
const myStore = createStore(myReducer, applyMiddleware(addOne, double));
console.log(myStore.getState());
myStore.dispatch(3);
console.log(myStore.getState());
applyMiddleware(addOne, double)
としてmyStore.dispatch
を実行するとまず、addOne
が呼ばれる。
action
にはmyStore.dispatch
に渡した値が入っており、これは既に説明した内容と同じ。
変わるのはnext
。Middleware が一つのときは本来のdispatch
が渡されていたが、今回のケースでは、double
を指す。
そのため以下のようになる。
const addOne = store => next => action => {
console.log(action);
next(action + 1);
};
そして呼び出されたdouble
。
action
には、先程のnext
に渡した値(action + 1
)が入っている。
そしてdouble
におけるnext
は、本来のdispatch
を指す。
const double = store => next => action => {
console.log(action);
next({type: ADD_COUNTER, value: action * 2});
};
const myReducer = (state = 0, action) => {
const {type, value} = action;
switch (type) {
case ADD_COUNTER:
console.log(action);
return state + value;
default:
return state;
}
};
このようにnext()
を使うことで、applyMiddleware
に渡した順番で Middleware を実行していくことになる。
その際、next()
の引数に値を渡すことで、任意の値をaction
として渡すことが出来る。
そして最後尾の Middleware がnext
を実行したときに、dispatch
が呼び出され、reducer
が実行される。
そのため、最後尾のnext()
の引数は、必ずプレーンなオブジェクトでないといけない。
途中の過程ではnext()
の引数はどんなものでも構わないが、最後尾のnext()
はそのままdispatch()
を呼び出すため、プレーンなオブジェクト以外を渡すとエラーになってしまう。
const double = store => next => action => {
console.log(action);
next(action * 2);
};
next の複数回使用、store.dispatch の使用
応用と言うほどの内容ではないが、こういう書き方も出来るという例を、復習も兼ねて書いておく。
next
は、一つの Middleware のなかで複数回使うことが出来る。
const twice = store => next => action => {
next(action);
next(action);
};
const myStore = createStore(myReducer, applyMiddleware(twice));
console.log(myStore.getState());
myStore.dispatch({type: ADD_COUNTER, value: 5});
console.log(myStore.getState());
Middleware に渡されたstore
はdispatch
メソッドを持っているが、これはmyStore.dispatch
と同じであり、これを呼び出すと再び先頭の Middleware から実行することになる。
const first = store => next => action => {
console.log('first', action);
next(action);
};
const second = store => next => action => {
console.log('second', action);
if (action < 3) {
store.dispatch(action + 1);
return;
}
next({type: ADD_COUNTER, value: action});
};
const myStore = createStore(myReducer, applyMiddleware(first, second));
console.log(myStore.getState());
myStore.dispatch(1);
console.log(myStore.getState());
Reactとの連携
最後に、ReactアプリでAPIを叩く例を書いてみる。
ReactとReduxの接続については、冒頭に貼った記事などを参照。
import React from 'react';
import ReactDOM from 'react-dom';
import {createStore, applyMiddleware} from 'redux';
import {Provider, connect} from 'react-redux';
const API_CALL = 'API_CALL';
const REFRESH_STATUS = 'REFRESH_STATUS';
function App({status, myActionCreateor}) {
return (
<div>
<p>通信状態:{status}</p>
<button
onClick={() => {
myActionCreateor(true);
}}
>
通信(成功する)
</button>
<button
onClick={() => {
myActionCreateor(false);
}}
>
通信(失敗する)
</button>
</div>
);
}
const myReducer = (state = {status: '通信前'}, action) => {
switch (action.type) {
case API_CALL:
return Object.assign({}, state, {
status: '通信中……',
});
case REFRESH_STATUS:
return Object.assign({}, state, {
status: action.status,
});
default:
return state;
}
};
const fetchApi = bool =>
new Promise((resolve, reject) => {
setTimeout(() => {
if (bool) {
resolve();
} else {
reject();
}
}, 3000);
});
const myActionCreateor = bool => ({type: API_CALL, result: bool});
const apiHandler = store => next => action => {
next(action);
if (action.type !== API_CALL) return;
fetchApi(action.result)
.then(() => {
next({type: REFRESH_STATUS, status: '通信成功'});
})
.catch(() => {
next({type: REFRESH_STATUS, status: '通信失敗'});
});
};
const myStore = createStore(myReducer, applyMiddleware(apiHandler));
const mapStateToProps = state => ({status: state.status});
const mapDispatchToProps = {myActionCreateor};
const ConnectedApp = connect(mapStateToProps, mapDispatchToProps)(App);
ReactDOM.render(
<Provider store={myStore}>
<ConnectedApp />
</Provider>,
document.querySelector('#app')
);
参考資料