React + Redux + flowで非同期処理

こちらの記事の続編です。
[React + Redux + flowの最小構成](http://qiita.com/uryyyyyyy/items/85c5f4e6d613bf1abbb0)

ソースコードはこちらになります。
https://github.com/uryyyyyyy/react-redux-js-sample/tree/async/

## この記事のゴール

前回の記事でreduxやテストの導入をしたので、非同期処理の書き方をそのテストをまとめていきます。

## 環境

- React 15.4
- webpack 2.2-rc
- flow 0.37
- NodeJS 6.X~
- mocha
- enzyme
- isomorphic-fetch

## 構成

こちらと全く同じです。

[React + Redux + flowの最小構成](http://qiita.com/uryyyyyyy/items/85c5f4e6d613bf1abbb0#%E6%A7%8B%E6%88%90)

## 非同期を扱うにあたって

ここでは、クライアントサイドでの非同期処理をサーバーとの通信に限ります。
サーバーとのやりとりをする上で、`XｍｌHttpRequest`の代わりにfetch APIというものが徐々にブラウザに実装されているので、ここではそれを使います。
また、非対応の実行環境のためにisomorphicなfetch polyfillを導入します。

```
npm install --save isomorphic-fetch
```

テストでもバンドル時にも使うので、polyfillは下記のようにまとめて置いておきましょう。Index.jsxやテストコードの中でこれをimportするだけで実行環境の差異をなくせます。

```js:polyfill.js
import "babel-polyfill";
import "isomorphic-fetch";
```

また、現在ECMA Scriptの方でstage3まで進んでいて、TypeScriptでも実装されておりChromeでも最近搭載された`async/await`の構文でjsコードを書けるようにしましょう。

```
npm install --save-dev babel-preset-es2016 babel-preset-es2017
```

これを.babelrcのpresetに突っ込み、バンドルする前にbabel-polyfillを噛ませれば `async/await`が使えるようになります。

## ソースコード

### mock server

fetchした時の動作確認用に、サーバーを立てます。ここでは高機能なexpressを用います。

```js:dev-server.js
const path = require('path');
const express = require('express');
const app = express();

app.use('/dist', express.static('dist'));

app.get('/api/count', (req, res) => {
  res.contentType('application/json');
  const obj = {"amount": 100};
  setTimeout(() => res.json(obj), 500);
  //res.status(400).json(obj); //for error testing
});

app.get('/', (req, res) => {
  res.sendFile(path.join(__dirname, 'index.html'));
});

app.listen(3000, (err) => {
  if (err) {
    console.log(err);
  }
  console.log("server start at port 3000")
});
```

ここではあえて500ms待つことで、読み込みに時間かかってる感を演出しています。

### ActionDispacher

さて、reduxでは非同期を扱う箇所はActionCreator（多くは、ミドルウェアというところに隠蔽する）で行うことになっています。なので、前回からの差分は主に`counter/Actions.js`の部分です。


```js:counter/Actions.js
...
export const FETCH_REQUEST_START = 'counter/fetch_request_start';
export const FETCH_REQUEST_FINISH = 'counter/fetch_request_finish';

const myHeaders = {
  "Content-Type": "application/json",
  'Accept': 'application/json',
  'X-Requested-With': 'XMLHttpRequest'
};

export class ActionDispatcher {
  dispatch: (action: any) => any;

  constructor(dispatch: (action: any) => any) {
    this.dispatch = dispatch
  }

...

  //①
  async fetchAmount(): Promise<void> {
    //②
    this.dispatch({type: FETCH_REQUEST_START});

    try {
      //③
      const response: Response = await fetch('/api/count', {
        method: 'GET',
        headers: myHeaders,
        credentials: 'include'
      });

      if (response.status === 200) { //2xx
        //④
        const json: JsonObject = await response.json();
        this.dispatch({type: INCREMENT, amount: json.amount})
      } else {
        //⑤
        throw new Error(`illegal status code: ${response.status}`);
      }
    } catch (err) { //⑥
      console.error(err.message);
    } finally { //⑦
      this.dispatch({type: FETCH_REQUEST_FINISH})
    }
  }
}
```

前回からの差分として見るべきところは`fetchAmount()` の部分です。
async/awaitを用いてそこそこわかりやすく書けたものの、順番に説明していきます。

①で、メソッドの頭にasyncが付いていて返り値がPromiseであることから、このメソッドが非同期処理をasync/awaitで取り扱うものであると宣言します。
②では非同期処理する前に、「Loading...」の画面を出すようにActionを飛ばします。
③でfetch APIを用いてサーバーへの通信処理（非同期）を行います。awaitを使うことで、Promiseを明示的に扱わなくて良くなります。

通信が上手く行って200が返ってきた場合は、④でデータを受け取って、reducerへIncrementのActionを飛ばしています。
通信は上手く行ったものの200以外（400など）が返ってきた場合は、そのステータスコードをエラーメッセージとして例外を投げています。
fetchが失敗した（サーバーの応答が無い、通信がそもそもできてないなど）の場合や、上記のエラーが投げられた時は⑥でそれを記録します。
最後に⑦で、①で行った「Loading...」表示を取り消すようにActionを発行しています。

Counter, Reducerはここでは省略しますが、特に変わってないので見ればすぐに分かるかと思います。

## テストコード

大きく変わったのがActionDispatcherなので、テストコードもそこがメインです。

```js:counter/__tests__/Actions.spec.js
import assert from 'assert';
import {ActionTypes} from "../Entities";
import {ActionDispatcher, INCREMENT, FETCH_REQUEST_START, FETCH_REQUEST_FINISH} from "../Actions";
import fetchMock from 'fetch-mock';
import {spy} from "sinon";

describe('ActionDispatcher', () => {

  //①
  beforeEach(() => {
    fetchMock.restore();
  });

...

  //②
  it('fetchAmount success',  async () => {
    //③
    fetchMock.get('/api/count', {body: {amount: 100}, status: 200});

    const spyCB:any = spy();
    const actions = new ActionDispatcher(spyCB);

    //④
    await actions.fetchAmount();

    const calls = spyCB.getCalls();
    assert(calls.length === 3);
    assert(deepEqual(calls[0].args, [{ type: FETCH_REQUEST_START }]));
    assert(deepEqual(calls[1].args, [{ type: INCREMENT, amount: 100 }]));
    assert(deepEqual(calls[2].args, [{ type: FETCH_REQUEST_FINISH }]));
  });

  it('fetchAmount fail',  async () => {
    //⑤
    fetchMock.get('/api/count', {body: {}, status: 400});

    const spyCB:any = spy();
    const actions = new ActionDispatcher(spyCB);

    await actions.fetchAmount();

    const calls = spyCB.getCalls();
    assert(calls.length === 2);
    assert(deepEqual(calls[0].args, [{ type: FETCH_REQUEST_START }]));
    assert(deepEqual(calls[1].args, [{ type: FETCH_REQUEST_FINISH }]));
  });
});
```

（polyfillはnpmスクリプトの中で読んでいるので、各種APIは使えるものとして進めて良いです。）

ここで、fetch-mockというライブラリを使っています。これは、テスト時に実際のサーバーへfetchしては困るので、それのmockをするためのライブラリです。
これを①で各テストの前に初期化していますね。

②では、テストが非同期処理を含むため、asyncを書いています。
ちなみにmochaでは、テストコードの返り値がPromiseだった場合、その処理が終わるまで待機してくれる性質があります。ここではasync定義のため必然的に返り値がPromise（returnを書かなければ`Promise<void>`）になるため、何もする必要はありません。

③ではfetchの結果として返したいものを用意しています。
④で実際に非同期処理を起こし、awaitで待機します。
そして、「loadingを出すためのアクション」「結果を取得したのでINCREMENTするアクション」「loloadingを消すアクション」が順番に発火していることをテストしています。

⑤ではfetchの結果が失敗だった時にも正しく処理されるかの確認となります。

## まとめ

reduxだと非同期でもテストが書きやすいですね。
また、async/awaitは読みやすくてすごく良いです。
Scalaをやっているのであれば、Futureでのfor式みたいなものと思えば理解しやすいと思います。