React Context APIを使った非同期通信のハンドリング

本稿は[React Advent Calendar 2019](https://qiita.com/advent-calendar/2019/react) 18日目の記事です！

## はじめに
Reactにおける非同期通信のハンドリングどうしていますか？  
通信中のローディングアイコンの表示や、エラーハンドリング・・・
正解がわからない🤔

そこで今回はReactのContext APIを使ってハンドリングしてみました！
これが正解だとは思いませんが、一例として共有させていただきます :pray:

## TL;DR
 - Redux使わないよ
 - Context APIでエラーハンドリングとダイアログコンポーネントの表示やってみたよ

## Reduxを使うパターン
よくありがちな、リクエスト毎に成功時と失敗時のアクションを用意するパターン。
Storeにエラー内容を突っ込んで、エラー表示のためのコンポーネントを作ってよしなにやるイメージ。

```jsx
const GET_ITEMS_REQUEST = 'GET_ITEMS_REQUEST';
const GET_ITEMS_SUCCESS = 'GET_ITEMS_SUCCESS';
const GET_ITEMS_FAILURE = 'GET_ITEMS_FAILURE';

const getItemsRequest = () => { ... }
const getItemsSuccess = () => { ... }
const getItemsFailure = () => { ... }

const getItems = () => {
  return (dispatch) => {
    dispatch(getItemsRequest);

    return axios.get(`http://localhost/api/items`)
      .then(res =>
        dispatch(getItemsSuccess(res.data))
      ).catch(err =>
        dispatch(getItemsFailure(err))
      );
  }
}

const initialState = { isFetching: false,  error: null, ...};

const reducer = (state = initialState, action) {
  switch (action.type) {
    case GET_ITEMS_FAILURE:
      return {
        ...state,
        isFetching: true,
      };
    case GET_ITEMS_FAILURE:
      return {
        ...state,
        isFetching: false,
        error: action.error,
      };
    ...
  }
}

// jsx 
{error && <Dialog>Error!!</Dialog>}
```

なんか冗長でしんどい😭


## Context APIで実装してみる
Context APIに関しては公式リファレンスをご参照くださいませ🙇‍♂️
[コンテクスト – React](https://ja.reactjs.org/docs/context.html)

以下、作ったものです！
https://codesandbox.io/s/loving-wiles-tvdjy?fontsize=14&hidenavigation=1&theme=dark

### index.js

APIのサンプルとして、QiitaのAPI叩かせてもらっています。
failureRequest内のpostは、tokenがなくて認証エラーになる形です。

```jsx
import React, { useContext } from "react";
import ReactDOM from "react-dom";
import axios from "axios";

import {
  ApiRequestHandleContext,
  ApiRequestHandleContextProvider
} from "./apiRequestHandleContext";

const successRequest = async params => {
  return axios.get("https://qiita.com/api/v2/items", { params });
};

const failureRequest = async () => {
  return axios.post("https://qiita.com/api/v2/items");
};

const App = () => {
  const { execRequest, isRequesting } = useContext(ApiRequestHandleContext);
  const handleOnSuccessClick = () => {
    // APIのレスポンスが返ってくる
    execRequest(successRequest, { page: 2 }).then(console.log);
  };

  const handleOnFailureClick = () => {
    execRequest(failureRequest);
  };

  return (
    <div className="App">
      <button onClick={handleOnSuccessClick}>Success Button</button>
      <button onClick={handleOnFailureClick}>Failure Button</button>
      {isRequesting && <div>Now Requesting!!</div>}
    </div>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(
  <ApiRequestHandleContextProvider>
    <App />
  </ApiRequestHandleContextProvider>,
  rootElement
);
```

### apiRequestHandleContext.js

```jsx
import React, { useState, createContext } from "react";
import Dialog from "./Dialog";

export const ApiRequestHandleContext = createContext({
  execRequest: () => {},
  isRequesting: false
});

export const ApiRequestHandleContextProvider = props => {
  const [isRequesting, setIsRequesting] = useState(false);
  const [errorMessage, setErrorMessage] = useState(null);

  const handleError = error => {
    setErrorMessage(error.response.data.message);
  };

  const execRequest = async (requestFn, ...args) => {
    setIsRequesting(true);
    const res = await requestFn(...args).catch(handleError);
    setIsRequesting(false);
    return res;
  };

  return (
    <ApiRequestHandleContext.Provider
      value={{
        isRequesting,
        execRequest
      }}
    >
      {errorMessage && <Dialog>{errorMessage}</Dialog>}
      {props.children}
    </ApiRequestHandleContext.Provider>
  );
};

```

## 解説
apiRequestHandleContextのexecRequestがポイント

```js
 const execRequest = async (requestFn, ...args) => {
    setIsRequesting(true);
    const res = await requestFn(...args).catch(handleError);
    setIsRequesting(false);
    return res;
  };
```

`requestFn`が実行する非同期通信処理で、可変長引数 `...args`をパラメータとして渡します。

非同期通信でエラーがあった場合、catchに渡されている`handleError`が実行され、エラーレスポンス内のmessageをDialogとして表示という仕組みになっています。

また、リクエストの前後でuseStateを利用してリクエスト中かのフラグ `isRequesting` をハンドリング。
このisRequestingはcontextとして提供されているので、リクエスト中はindex.js側で「Now Requesting!!」というテキストを表示しています。

あとは `ApiRequestHandleContextProvider` でラップしてあげれば🙆‍♂️

```js
const { execRequest, isRequesting } = useContext(ApiRequestHandleContext);
  const handleOnSuccessClick = () => {
    // execute someAsyncFunction(param1, param2, param3);
    execRequest(someAsyncFunction, param1, param2, param3}).then(res => { console.log(res)});
  };
```

## 注意
今回の実装だと、並列で複数のリクエストが呼ばれた際にリクエスト状態は1つのisRequestingを参照しているので、実際はまだ終了していないリクエストがある場合もisRequestingはfalseになってしまいます。

コードが複雑になるのを避けたかったので今回は実装していませんが、リクエスト毎にユニークキーを振って、それぞれのリクエストの状態を1つずつ管理するような実装もしたりしました :innocent: 

## おわりに
 - ロジックをview側に寄せる形になるので抵抗ある人はあるかも・・・
 - でもReducerを肥大化させるのも辛い😭
 - たぶん色々なハンドリングパターンがあると思うので、もっと色々調べてみたい
 - Hooksは偉い！😘