23.React/Redux|async/await

asyncの処理の中で呼び出している関数の頭にawaitが抜けていて、他のawaitの処理と順番が入れ変わってしまうという問題があった。
今回はJavaScriptにおけるasync/awaitについて、React/Reduxでの書き方も含めて確認したいと思う。

まずはasync/awaitと密接に結び付いている、Promiseの処理から確認していく。

Promise

JavaScriptの非同期処理の仕組みであり、成功時と失敗時にそれぞれコールバック関数を渡しておくことで、
非同期処理の終了時にそれらを実行してくれる。

  • 下記のように非同期処理を実装することができる。
const promise = new Promise((resolve, reject) => {
  // 処理
  
  if (/* 成功したかどうかの判定 */) {
    resolve(/* 処理結果 */);
  } else {
    reject(/* エラーオブジェクト */);
  }
});
  • しかし複数の非同期処理を組み合わせる際に、このコールバック処理は複雑化しやすくバグを生みやすくなる。
fetch("/api/hoge")
.then(response => {
  // responseからidを抽出する
  const hogeId = translateResponse(response);
  // hogeIdを使って別のAPIを呼び出す
  return fetch(`/api/fuga?hogeId=${hogeId}`);
})
.then(response => {
  // responseからidを抽出する
  const fugaId = translateResponse(response);
  // hogeIdとfugaIdを使って別のAPIを呼び出したいが、
  // hogeIdがこのスコープにないのでエラーが起きる
  return fetch(`/api/piyo?hogeId=${hogeId}&fugaId=${fugaId}`);
})

そこで、async/awaitを用いる方法を見ていきたい。

async/await構文

JavaScriptのベース(技術標準)となっているECMASCript
その中でも2017年に公開されたECMAScript 2017(ES8)以降、JavaScriptでこのasync/await構文が使えるようになった。

asyncとは、非同期関数を定義する関数宣言を指す。

async function sample() {
  return 1;
}

awaitは、async function内でPromiseの結果(resolve/reject)が返されるまで待機する(処理を一時停止する)演算子のこと。
async関数の中でのみ動作する。

async function sample() {

  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("done!"), 1000)
  });

  let result = await promise; // promise が解決するまで待機

  alert(result); // "done!"
}

sample();

上記のように複数の非同期処理も組み合わせて簡潔に書くことができる。

ja.javascript.info

React/Redux:async/await

React/Reduxの中で使う場合にも考え方は同じ。
下記のように書くことができる。

import React, { useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { unwrapResult } from '@reduxjs/toolkit'

import { addNewPost } from './postsSlice'

export const AddPostForm = () => {
  const [title, setTitle] = useState('')
  const [content, setContent] = useState('')
  const [userId, setUserId] = useState('')
  const [addRequestStatus, setAddRequestStatus] = useState('idle')

  // omit useSelectors and change handlers

  const canSave =
    [title, content, userId].every(Boolean) && addRequestStatus === 'idle'

  const onSavePostClicked = async () => {
    if (canSave) {
      try {
        setAddRequestStatus('pending')
        const resultAction = await dispatch(
          addNewPost({ title, content, user: userId })
        )
        unwrapResult(resultAction)
        setTitle('')
        setContent('')
        setUserId('')
      } catch (err) {
        console.error('Failed to save the post: ', err)
      } finally {
        setAddRequestStatus('idle')
      }
    }
  }

  // omit rendering logic
}

redux.js.org