28
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ReactAdvent Calendar 2019

Day 18

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

Posted at

本稿はReact Advent Calendar 2019 18日目の記事です!

はじめに

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

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

TL;DR

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

Reduxを使うパターン

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

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://codesandbox.io/s/loving-wiles-tvdjy?fontsize=14&hidenavigation=1&theme=dark

index.js

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

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

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がポイント

 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 でラップしてあげれば🙆‍♂️

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は偉い!😘
28
22
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?