redux-thunkで非同期処理を実現する
redux-thunkはReduxにMiddlewareとして組み込むために開発されているライブラリです。通常、dispatch関数にはActionのオブジェクトしか渡すことができませんが、このMiddlewareを適用したRedux環境では、dispatch関数に関数オブジェクトを渡すことができるようになります。これによって何がうれしいのかというと、Action Creatorの作り方が変わるのです。リスト7に例を挙げます。
// redux-thunk使用前のAction Creator
function doSomething() {
return { type: "SOMETHING" }
}
// redux-thunk使用後のAction Creator
function doSomething() {
return (dispatch) => { // (1)
fetch("https://example.com/api/hoge")
.then(response => {
// 非同期処理の後にdispatchを呼べる
dispatch({ type: "SOMETHING", payload: response.ok }); // (2)
})
}
}
redux-thunk使用後、Action Creatorが返却する関数オブジェクトには、dispatch関数が渡されます(1)。このdispatch関数を使うことで、関数オブジェクトの内部で実施した非同期処理の結果を用いたdispatch呼び出しが可能となります(2)。Reactからは通常のAction Creatorと同じようにdispatch(doSomething())といった呼び出し方をしていますし、Reducer側から見ても従来通りの方法でActionが発行されたように見えるはずです。
このように、dispatchした後、かつReducerが処理する前のタイミングで、非同期処理を許容しながらActionを加工・生成していくのが、Middlewareという仕組みの真骨頂です。Middlewareをちゃんと理解しようとすると大変ですが、「そういった特性を持ったものだ」とだけ理解しておけば、他のMiddlewareを採用する際にも役割を理解しやすいのではないかと思います。
redux-thunkを導入する
それでは、redux-thunkをアプリケーションに組み込んでいきましょう。reduxとreact-reduxは導入済みであるものとします。まずは、リスト8のようにredux-thunkをインストールしてください。
$ npm install redux-thunk --save
次に、前回も使用したReduxのcreateStore関数を使って、redux-thunkを組み込みます(リスト9)。
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';
const store = createStore(
rootReducer,
applyMiddleware(thunk) // (1)
);
ポイントは、createStore呼び出しの第二引数に、applyMiddleware(thunk)でredux-thunkのモジュールを渡しているところです(1)。他のMiddlewareを導入するときには、applyMiddleware関数に渡す引数を増やすことで対応できます。
さて、これでredux-thunkを使う準備は完了です。もうdispatch関数に関数オブジェクトを渡せるようになりました。
非同期処理を実装する
それではredux-thunkを使った非同期処理を実装していきましょう。リスト6で使っていたinitPostsとsendNewPostを実装していきます(リスト10)。今回は実際にはサーバーと通信しているわけではありませんが、それらしく見えるよう、データ取得用のApiClient.fetchPosts()とデータ送信用のApiClient.sendPost(newPost)を用意しました。それらしく動くようにもしてあるので、興味がある方はサンプルコードを御覧ください。
/** サーバーのデータを取得して内部状態を初期化する */
export function initPosts() {
return (dispatch) => {
// 最新の投稿データすべてをサーバーから取得する
ApiClient.fetchPosts() // (1)
// 成功したらReducerに投稿データを渡す
.then(posts => dispatch(updatePosts(posts))) // (2)
// 失敗したらReducerに失敗したことを通知する
.catch(e => dispatch(initFailed(e))) // (3)
}
}
/** サーバーに新しい投稿を送信する */
export function sendNewPost(newPost) {
return (dispatch) => {
// 投稿データをサーバーに送信する
ApiClient.sendPost(newPost) // (1)
// 送信に成功したら送信後の投稿データすべてをサーバーから取得する
.then(() => ApiClient.fetchPosts()) // (1)
// データの取得に成功したらReducerに投稿データを渡す
.then(posts => dispatch(updatePosts(posts))) // (2)
// 失敗したらReducerに失敗したことを通知する
.catch(e => dispatch(sendingFailed(e))) // (3)
};
}
さて、初期化も送信も、基本的な作りは同じです。非同期処理を実行する(1)、成功していれば成功した事実やそれによって得られたデータを別のAction Creatorを使ってdispatchする(2)、失敗したら失敗した事実やエラーオブジェクトを別のAction Creatorを使ってdispatchする(3)、といった流れになっています。
リスト10の中でのdispatchに使っているAction Creatorの実装は、リスト11の通りです。至って普通のStandard Flux Actionsになっています。
/** 最新の全投稿データをアプリ内の状態に反映する */
function updatePosts(latestPosts) {
return {
type: "UPDATE_POSTS",
payload: {
posts: latestPosts
}
}
}
/** 初期化に失敗した */
function initFailed(error) {
return {
type: "INIT_FAILED",
payload: error,
error: true
}
}
/** 送信に失敗した */
function sendingFailed(error) {
return {
type: "SENDING_FAILED",
payload: error,
error: true
}
}
また、リスト11のActionを受け取るReducerはリスト12のようになります。
const defaultState = { posts: [], error: false };
const posts = (state = defaultState, action) => {
switch(action.type) {
case 'UPDATE_POSTS':
return {
posts: action.payload.posts,
error: false, // 前回が失敗しているかもしれないので明示的にfalseを指定する
};
case 'INIT_FAILED':
return {
...state,
error: true
};
case 'SENDING_FAILED':
return {
...state,
error: true
}
default:
return state;
}
};
export default posts;
Reducerの特筆すべき点としては、エラーがあったかどうかを表すerrorプロパティを持っていることです。リスト4でも触れた通り、Reducerでエラーの有無を管理しておくと、コンポーネント側でエラーがあったことをユーザーに伝える際に便利です。
これで非同期処理を伴うReduxが一回り分実装できました。図5でブラックボックスになっていた部分は、図6の流れで実装されたことになります。
React側からのdispatch呼び出しをそのまま使うのではなく、一度別の処理を行ってから改めてdispatchを行う、という特徴をつかんでおきましょう。
まとめ
今回は、非同期処理の手法について紹介しました。Reduxの非同期処理にはデファクトスタンダードが確立しておらず、さまざまな手法が乱立しています。その中でも比較的人気の高いredux-thunkを使った方法を紹介しました。
次回はMiddlewareを使わない方法の代表例として、ES2017のasync/await構文を使った非同期処理について解説します。
