概要
React + Redux のサンプルを使って、Middleware の作成と動作の確認をします。
準備
Reducer 編の続き(tag/reducer
)から行います。
※ 実装完了はtag/middleware
メッセージダイアログを表示する Reducer の作成を行います。
Middleware とは
Reducer の実行前後に処理を追加するための仕組みです。
Middleware は次のような形式です。
store => next => action => {
:
}
next
は次の middleware を表します。 store
と action
は名前の通りです。
何もしない middleware は次のようになります。
store => next => action => {
next(action);
}
middleware はすべての Action に適用されるため、これを基本形(つまり何もしない)にして、前後に追加の処理をしたり、特定の条件の場合のみ別の処理をすることになります。
next
は middleware のチェインなので、次々と middleware が呼ばれ、最終的には dispatch (さらにその先の Reducer) が呼ばれます。
つまり、1つの middleware で見ると、次のようになります。
store => next => action => {
// 前の state
next(action);
// 後の state
}
例えば、reducer の実行前後のログを取りたい場合、次のようになります。
export const logger = store => next => action => {
console.log("before: %O", store.getState());
next(action);
console.log("after: %O", store.getState());
};
middleware は次のようなことができます。
-
next()
を複数回呼ぶ -
next()
を非同期で呼ぶ -
next()
に新しい Action を渡す
この性質を利用して、API 通信を middleware で行い、「通信前に通信中状態の Action」、通信結果に応じて、「通信結果の Action」「通信失敗状態の Action」「リトライ」などの処理を middleware に閉じ込めることができます。
Middleware の作成
上記の logger
をサンプルに追加してみます。
middleware/index.js
を作成します。
export const logger = store => next => action => {
console.log("before: %O", store.getState());
next(action);
console.log("after: %O", store.getState());
};
store/configureStore.dev.js
に middleware を追加します。
※ 高機能なロガーの createLogger()
はコメントアウトしてください。
import { logger } from "../middleware"
const finalCreateStore = compose(
applyMiddleware(logger), // 追加
applyMiddleware(thunk),
reduxReactRouter({ routes, createHistory }),
//applyMiddleware(createLogger()), // コメントアウト
DevTools.instrument()
)(createStore);
実行して、Action を実行すると console にログが表示されます。
非同期処理
本当の通信は早すぎるので、タイマーを使ったダミーのサンプルを作成します。
まず、「通信中」「通信結果」の state を作る Reducer と、その Action を作ります。
const aServiceInitial = {
isFetching: false,
data: ""
};
function aService(state = aServiceInitial, action) {
const { type, data } = action;
switch (type) {
case ActionTypes.SERVICE_GET_DATA:
return Object.assign({},
state,
{
isFetching: true
});
case ActionTypes.SERVICE_SUCCESS:
return {
isFetching: false,
data: data
};
default:
return state;
}
}
export const SERVICE_GET_DATA = "SERVICE_GET_DATA";
export const SERVICE_SUCCESS = "SERVICE_SUCCESS";
export function serviceGetData(title, message) {
return {
type: SERVICE_GET_DATA,
api: "http://example.com",
success: serviceSuccess
}
}
export function serviceSuccess(data) {
return {
type: SERVICE_SUCCESS,
data: data
}
}
さらに2つの state を表示する UI と Action を発行するボタンを追加します。
handleFetch(e) {
this.props.serviceGetData();
e.preventDefault();
}
renderFetch() {
const { aService } = this.props;
return (
<div>
<button type="button" className="btn btn-primary btn-lg" onClick={::this.handleFetch}>
fetch
</button>
<div>data : {aService.data}</div>
<div>{aService.isFetching ? "fetching..." : ("")}</div>
</div>
);
}
middleware の仕様として Action に action.api
があればその url に通信し、action.success
に受信したデータを渡すことにします。
export const api = store => next => action => {
next(action);
if (action.api) {
setTimeout(() => {
next(action.success(store.getState().aService.data + store.getState().aService.data.length))
}, 3000);
}
};
1つ目の next(action)
は何もしない処理で api
プロパティを持っていない場合は、ここだけが実行され何も影響を与えません。
SERVICE_GET_DATA
の場合も一旦何もせず Reducer を実行させることで isFetching
を true
にし、UI に "fetching..." と表示させています。
次に action.api
があるときは、3秒後(通信のシミュレーションです)に next(action ~~)
を呼んでいます。
SERVICE_GET_DATA
の場合、serviceGetData()
が action.success
にセットされているため、SERVICE_SUCCESS
Action が実行されます。
最後に
middleware は Reducer (実際はdispatch) を非同期に実行したり、Action を変えたりできます。
うまく設計することで、Action や Reducer を単純にできます。