Reactでカスタムフックを作ってAPIを共通化する方法

## 最初はEndPointごとにAPIアクセス処理を作成しようとしていた

サーバ側のAPIでデータを取得しようとするときに、API毎にAPIアクセス処理を作成しようとしていました。下記のような感じです。

下記の例は、News情報を`https://example.com/api/news`に取得し一覧に表示します。もしエラーが出たら`error`ページに遷移させるというものです。

```tsx:news.tsx
import { useEffect, useState } from "react";
import Router from "next/router";
import NewsApi from "newsApi";

const NewsPage = () => {
  const [list, setList] = useState<News[]>([]);
  useEffect(() => {
    const func = async () => {
      const res = await NewsApi.list();
      if (res.status !== 200) {
        Router.push("/error");
      } else {
        setList(res.data);
      }
    };
    func();
  }, []);
  return (
    <ul>
      {list &&
        list.map((n) => (
          <li key={n.newsID}>
            <p>{n.startTime}</p>
            <dl>
              <dt>{n.title}</dt>
              <dd>{n.description}</dd>
            </dl>
          </li>
        ))}
    </ul>
  );
};
```

```typescript:newsApi.ts
import axios from "axios";
import News from "news";

class NewsApi {
  list = async () => {
    const res = await axios.get<News[]>("https://example.com/api/news", {
      headers: getAuthHeader(),
    }).catch((err) => {
        return err.response;
      });
    return res;
  };
}

export default new NewsApi();
```

```typescript:news.ts
type News = {
  newsID: number;
  title: string;
  description: string;
  linkURL: string;
  startTime: Date;
  endTime: Date;
};

export default News;
```

## カスタムフックで共通化する

私の作っているAPIの共通仕様として、ヘッダーに認証用の`getAuthHeader()`を付与するというものがありました。
また、エラーページに遷移させるというのも各API共通のものとなります。
そうすると、毎回書くのは手間だったり、間違いが起こったりする可能性がありますので、API毎にAPIアクセス処理を用意するのではなく、共通化したものを用意した方がいいのではないかと。

カスタムフックと用いて実装したいと思います。

> カスタムフックとは、名前が ”use” で始まり、ほかのフックを呼び出せる JavaScript の関数のことです。
https://ja.reactjs.org/docs/hooks-custom.html

ということで、useApi.tsを新たに作成しました。
axiosのインスタンス（`httpClient`）を使ってもらって、呼び出し元で定義するように修正しています。

```typescript:useApi.ts
import axios, { AxiosResponse } from "axios";
import Router from "next/router";
import { useState, useEffect } from "react";

export let httpClient = axios.create({
  headers: getAuthHeader(),
});

const useApi = <T>(
  path: string,
  axiosFunc: () => Promise<AxiosResponse<T>>,
  initialState: T,
  handleError: ((res) => void) | null = null
): T => {
  const [data, setData] = useState<T>(initialState);
  useEffect(() => {
    const func = async () => {
      const res = await axiosFunc().catch((err) => {
        return err.response;
      });
      if (res.status !== 200) {
        handleError ? handleError(res) : Router.push("/error");
      } else {
        setData(res.data);
      }
    };
    func();
  }, []);
  return data;
};

export default useApi;

```

news.tsxはこう変わりました。

```tsx:news.tsx
import News from "news";
import useApi, { httpClient } from "useApi";

const NewsPage = () => {
  const path = "https://example.com/api/news";
  const req = () => {
    return httpClient.get(path);
  };
  const list = useApi<News[]>(path, req, []);
  return (
    <ul>
      {list &&
        list.map((n) => (
          <li key={n.newsID}>
            <p>{n.startTime}</p>
            <dl>
              <dt>{n.title}</dt>
              <dd>{n.description}</dd>
            </dl>
          </li>
        ))}
    </ul>
  );
};
```
![node.js-12.8.0](https://img.shields.io/badge/node.js-12.8.0-brightgreen) ![Typescript-3.9.7](https://img.shields.io/badge/Typescript-3.9.7-brightgreen) ![next.js-9.4.4](https://img.shields.io/badge/next.js-9.4.4-brightgreen)