Reactコンポーネント内でしか使わないAPIをaxiosで簡単に呼ぶための独自Hookを書きました

## (更新) SWR使うといいらしい

next.js周りを追ってて知ったんですが `SWR` というライブラリを使うと割と上位互換的な事が出来るみたいでした。

https://swr.vercel.app/

本記事については、型パズルの側面などで情報としての有用性はありそうなので、このまま残しておきます。

## 概要 (TL;DR)

コンポーネント内だけでAPI呼び出しして使いたい時に便利なHook書いたのでよかったら使ってください。

redux使うまでもない時は楽だと思います。

## 経緯とか細かい話

ReactといえばRedux,ReduxといえばSaga...

それでもWebはReact以前からあって、昔ながらの画面遷移していくページも作ることはありますよね。
そういう昔ながらのUIだと、画面間で一切データを持ち越さないようなAPI呼び出しをすることも多いです。

これを毎回reduxで書くのは大変な割りに旨味が少ないです。ducksパターンやなどを用いてもボイラープレートコードの嵐になり、お決まりの `@namespace/INDEX_REQUEST` `@namespace/INDEX_SUCCESS` `@namespace/INDEX_FAILURE` を書くことになります。今この記事のために書いてるだけでもちょっと面倒です。しかも、そのコンポーネントを抜けるときにcleanした方が良いかなとか、うっかりloading等の状態管理を書き間違えたりとか、やりがちです。

もちろん一般論としては、reduxを使うメリットは沢山あります。良い具合のmiddlewareでロード状態を表現したりするのも魅力的ですが（mastodonのコードを読むと結構やってる）、ほぼ1つの画面から動かない(カラム式のTweetDeck的な)画面レイアウトだからこそ活きるというのもあります。
後からステートを他のビューからも使い回したくなったら楽です。テストも書きやすいです。

けれども、ひとつの画面に閉じた状態を持ちたい場合、reduxをそこに使うのは割と不毛だと思います。

こういったPros/Consがあることを承知しながら、ごく簡単な(Railsでいうscaffoldの延長みたいな)API呼び出しをするだけのReact Hooksの独自フックを書いたら良い感じに仕上がったと思うので紹介します。

言語はTypeScriptです。 vanilla JS派の型は [TypeScript Playground](https://www.typescriptlang.org/play) あたりで変換してください。 Flow使ってるような強い人はサッと書き換えれると思ってます。

## コード本体

```ts
import { useEffect, useState } from "react";
import { AxiosPromise } from "axios";
import { PromiseType } from "utility-types";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type APIFunc = (...args: any[]) => AxiosPromise;

type APIDataTypeOf<Fn extends APIFunc> = PromiseType<ReturnType<Fn>>["data"];

interface UseAPIResult<Data> {
  loading: boolean;
  finished: boolean;
  data?: Data;
  error?: Error;
}

interface UseAPIRequestResult<Fn extends APIFunc>
  extends UseAPIResult<APIDataTypeOf<Fn>> {
  request: (...args: Parameters<Fn>) => void;
}

export function useAPIRequest<Fn extends APIFunc>(
  api: Fn
): UseAPIRequestResult<Fn> {
  const [result, setResult] = useState<UseAPIResult<APIDataTypeOf<Fn>>>({
    loading: false,
    finished: false
  });

  const request = (...args: Parameters<Fn>) => {
    return api(...args)
      .then(result => {
        setResult({
          data: result.data,
          loading: false,
          finished: true
        });
      })
      .catch(error => {
        setResult({
          error,
          loading: false,
          finished: true
        });
      });
  };

  return {
    ...result,
    request
  };
}

export function useAPI<Fn extends APIFunc>(
  api: Fn,
  ...args: Parameters<Fn>
): UseAPIResult<APIDataTypeOf<Fn>> {
  const { request, ...result } = useAPIRequest<Fn>(api);

  useEffect(() => {
    request(...args);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);
  return result;
}
```

### 使い方

※ざっくり書いたのでもし動かなかったら教えてください。

```jsx
const Show: React.FC<Props> = props => {
  const classes = useStyles();

  const uuid = props.match.params.uuid;

  const { loading, error, data } = useAPI(axios("YOUR_URL"));
  // NOTE: 即時ではなく後からuseEffect内とかでAPIを呼び出したいときは以下の感じ
  const { loading, error, data, request } = useAPI(axios("YOUR_URL"));
  useEffect(()=>{
    // パラメタわたすなら request(param) で渡せます
    request()
  }, [YOUR_CONDITION])

  return (
    {JSON.stringify(data)}
  );
};
export default Show;
```

尚、openapi-generatorでaxios-typescriptの結果で出てくるAPIシグネチャが可変長引数を取る関係で、そこにも重点的に対応してます。 ですので、軽くPromiseまわり書き換えるだけで大抵のPromiseフレンドリーなAPIライブラリで使えると思います。Promise(async関数)でwrapして、APIDataTypeOfまわりを調節すればよさそうです。

### ライセンス

MITライセンスとWTFPLライセンスのデュアルライセンスで提供します。つまり気にせずコピペして頂いて構いません。

（いつも思うんだけど、こういう記事でライセンス明記されてないと仕事で使うとき困りません？）