async/await構文で掲示板アプリを作り直す
本記事では、第9回で非同期処理に対応させた掲示板アプリを、async/await構文を用いて作り直します。といっても、変わった点はあまり多くありません。srcフォルダへの変更内容をまとめたのがリスト6です。
src/
actions/
posts.js (▲)
api/
ApiClient.js
reducers/
index.js
posts.js
App.css
App.js (▲)
Form.js
index.js (▲)
List.js
▲が付いているファイルが、今回変更があったものです。Form.jsやList.jsだけでなく、今回はreducersフォルダにも変更がありません。第9回でもお話した通り、Reduxと非同期処理の関係についての話題は「非同期処理の結果をいつどうdispatchするか」に収束します。そのため、dispatch後の挙動であるreducerには影響が起こらないのです。
Middlewareを使わない
まずは、index.jsを見てみましょう(リスト7)。
const store = createStore(rootReducer);
もともとredux-thunk以外のMiddlewareを使っていなかったので、applyMiddlewareを実行する必要がなくなり、従来通りのcreateStoreの使い方で動くようになりました。
コンポーネントにdispatchをさせない
次にApp.jsの挙動がどうなるのか見てみましょう(リスト8)。
import { initPosts, sendNewPost } from "./actions/posts";
class App extends Component {
componentDidMount() {
// 投稿を表示するための初期データをサーバーに請求する
initPosts(this.props.dispatch); // (1)
}
/** Formが作成した投稿を保存する処理 */
saveNewPost(newPost) {
// サーバーにデータを送る
sendNewPost(this.props.dispatch, newPost); // (2)
}
// 略
}
これまではコンポーネント内の適切なタイミングでdispatchを実施するスタイルを取っていましたが、Middlewareがない環境でdispatchすると、何の処理も挟むことができないままReducerに届いてしまいます。これは望んだ動作ではありません。そのため(1)や(2)のように、Action Creator(のような別物)を実行する際に、dispatch関数を引数で渡して、initPostsやsendNewPostの内部実装にdispatchのタイミングを委ねる方式にしました。App.jsへの変更はこれだけです。
非同期処理をasync functionで記述する
それでは、コンポーネントから呼び出された、initPostsやsendNewPostの内部実装を見てみましょう(リスト9)。
/** サーバーのデータを取得して内部状態を初期化する */
export async function initPosts(dispatch) {
try {
// 最新の投稿データすべてをサーバーから取得する
const posts = await ApiClient.fetchPosts();
// 成功したらReducerに投稿データを渡す
dispatch(updatePosts(posts));
} catch(e) {
// 失敗したらReducerに失敗したことを通知する
dispatch(initFailed(e));
}
}
/** サーバーに新しい投稿を送信する */
export async function sendNewPost(dispatch, newPost) {
try {
// 投稿データをサーバーに送信する
await ApiClient.sendPost(newPost);
// 送信に成功したら送信後の投稿データすべてをサーバーから取得する
const posts = await ApiClient.fetchPosts();
// データの取得に成功したらReducerに投稿データを渡す
dispatch(updatePosts(posts));
} catch (e) {
// 失敗したらReducerに失敗したことを通知する
dispatch(sendingFailed(e));
}
}
async functionで再実装されたinitPostsやsendNewPostの内部では、awaitを使ってAPIの呼び出しを待てるようになりました。成功・失敗の挙動の際にdispatchするactionについては、第9回で紹介したものとまったく同じものを使っています。
App.jsから呼び出した場合は各関数からPromiseが戻り値として返っていますが、今回の使い方ではawaitしながらdispatchを行うのが目的なので、あえてApp.js側でthenを使うことはしていません。成功・失敗を受けて表示を更新するのは、あくまでもRedux経由で行いましょう、というスタンスです。
これで、async/await構文による非同期処理を組み込むことができました。
[コラム]実はredux-thunkと併用してもよい
「redux-thunk + Promise」を使う方法とasync/awaitを使う方法をそれぞれ別個に紹介してきましたが、実はこれらは併用することも可能です(リスト10)。
// src/App.js
this.props.dispatch(initPosts());
// src/actions/posts.js
export function initPosts() {
return async (dispatch) => { // (1)
try {
const posts = await ApiClient.fetchPosts();
dispatch(updatePosts(posts));
} catch (e) {
dispatch(initFailed(e));
}
}
}
(1)のように、ラムダ式の頭にasyncキーワードを付けるのがコツです。これで、redux-thunkを使ってdispatchを受け取るためのラムダ式をasync functionにすることができ、内部でawaitを使えるようになります。外側のinitPosts関数自体にasyncキーワードを付けてもラムダ内でawaitが使えるようになるわけではないので、ご注意ください。
redux-thunkをはじめとしたMiddlewareたちは、async/awaitと排他的なツールではありません。適切に組み合わせながら、開発しやすい環境を組み立てていきましょう。
まとめ
2回にわたってReduxの非同期処理について紹介してきました。「実際に非同期処理をしているのはPromiseやasync/awaitであって、Reduxは何もしていないじゃないか」と思った方は、Reduxへの理解度がかなり高まっています。大切なのはReactコンポーネント内に非同期処理を書かないことだけで、そうでない場所でPromiseやasync/awaitで非同期処理を行い、最後にdispatchが呼び出せれば、本質的にはどんな書き方でもよいのです。皆さんも自分にあった非同期処理のスタイルを模索してみてください。
次回は、Reduxで書いたコードをユニットテストする手法をご紹介します。
