本稿はReact Advent Calendar 2019 18日目の記事です!
はじめに
Reactにおける非同期通信のハンドリングどうしていますか?
通信中のローディングアイコンの表示や、エラーハンドリング・・・
正解がわからない🤔
そこで今回はReactのContext APIを使ってハンドリングしてみました!
これが正解だとは思いませんが、一例として共有させていただきます
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つずつ管理するような実装もしたりしました
おわりに
- ロジックをview側に寄せる形になるので抵抗ある人はあるかも・・・
- でもReducerを肥大化させるのも辛い😭
- たぶん色々なハンドリングパターンがあると思うので、もっと色々調べてみたい
- Hooksは偉い!😘