React プロジェクト全体で axios の設定を共有する

# 問題設定

様々なケースについて、axios の設定を React プロジェクト全体で共有する方法について考えます。

# 実装したもの

実装したものはこちらにまとめてあります。

https://github.com/yuinore/axios_provider/pull/2/files

デモサイト：

https://axios-provider.yuinore.net/

# 実装手順

## 1. export で変数を共有する

axios の共通設定は `axios.defaults` で設定できます。

```tsx:src/components/MyComponent.tsx
axios.defaults.baseURL = 'https://api.waifu.pics/';
axios.defaults.headers.common.Authorization = `Bearer <authenticity-token>`;
```

ES Modules を使用していれば、このようなグローバルに参照される変数は export で共有することができます。

```tsx:src/components/MyAxios.tsx
import axios from 'axios';

const myAxios = axios.create();
myAxios.defaults.baseURL = 'https://api.waifu.pics/';
myAxios.defaults.headers.common.Authorization = `Bearer <authenticity-token>`;

// グローバルに使用される変数を他のファイルから読み込めるようにする
export default myAxios;
```

```tsx:src/components/MyComponent.tsx
import myAxios from './MyAxios';

function MyComponent(): JSX.Element {
  myAxios.get(...).then(...); // myAxios を使用した API 呼び出し
}
```

このように初期化処理をトップレベルスコープに書き、ファイルの最初で import すれば、必ず myAxios の初期化が先に実行されることを保証できます。設定が固定値の場合はこの実装で問題ありません。

あるいは、そもそも axios オブジェクトは create() で作成しない限り 1つしか存在しないため、単に index.tsx や App.tsx で初期値を設定するだけで構いません。

```diff_tsx:src/index.tsx
  import React from 'react';
  import ReactDOM from 'react-dom/client';
  import App from './App';
+ import axios from 'axios';
+
+ axios.defaults.baseURL = 'https://api.waifu.pics/';
+ axios.defaults.headers.common.Authorization = `Bearer <authenticity-token>`;

  const root = ReactDOM.createRoot(
    document.getElementById('root') as HTMLElement
  );
  root.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>
  );
```

## 2. 関数コンポーネント内で初期化する

この初期化処理を関数コンポーネント内に書くことを考えましょう。例えば `useCookies` を使用し、 `cookies.token` の値で `axios.defaults.headers.common.Authorization` の値を初期化する場合、初期化処理をトップレベルスコープではなく関数コンポーネントの内部に実装する必要があります。このために、中間のコンポーネントとして AxiosProvider を導入します。

```diff_tsx:src/index.tsx
+ import AxiosProvider from './components/AxiosProvider'

  root.render(
    <React.StrictMode>
-     <App />
+     <AxiosProvider>
+       <App />
+     </AxiosProvider>
    </React.StrictMode>
  );
```

```tsx:src/components/AxiosProvider.tsx
import axios from 'axios';

function AxiosProvider(props: { children: React.ReactNode }): JSX.Element {
  // 例えば、useCookies を使用する必要があるかもしれない
  // const [cookies, ..., ...] = useCookies(...);
  // const authenticity_token = ...;

  axios.defaults.baseURL = 'https://api.waifu.pics/';
  axios.defaults.headers.common.Authorization = `Barer <authenticity_token>`;

  return (
    <>
      {props.children}
    </>
  );
}

export default AxiosProvider;
```

API 呼び出しは useEffect の中に記述します。

```diff_tsx:src/components/MyComponent.tsx
  import axios from 'axios';

  function MyComponent(): JSX.Element {
-   axios.get(...).then(...); // axios を使った API 呼び出し
+   // useEffect は全コンポーネントのレンダリングが完了した後に呼び出される
+   useEffect(() => {
+     axios.get(...).then(...); // axios を使った API 呼び出し
+   }, []);
}
```

このように書くことで、初期化処理が完了してから useEffect 内の API 呼び出しが実行されることを保証できます。

## 3. useContext を使用する

さて、この方法の問題点は、最初の読み込みが終わった後に変数に変更があっても React がそれを検知できず、コンポーネントを再レンダリングすることができないという点です。結果として、画面に表示された情報は古いままとなってしまいます。

変数を prop として渡せばもちろん変更を子コンポーネントに伝えることはできますが、このケースではコードが冗長になってしまうでしょう。

一般的に、このようなグローバルな変数を React 全体で共有するために useContext が使用できます。

```tsx:src/components/AxiosContext.tsx
import React from 'react';
import axios from 'axios';

// 引数に初期値 axios を与えているが、実際にはこの値は参照されない。
const AxiosContext = React.createContext(axios);

export default AxiosContext;
```

```tsx:src/components/AxiosProvider.tsx
import axios from 'axios';
import AxiosContext from './AxiosContext';

let myAxios = axios.create();
let isAxiosDirty = true;

function AxiosProvider(props: { children: React.ReactNode }): JSX.Element {
  // 例えば、useCookies を使用する必要があるかもしれない
  // const [cookies, ..., ...] = useCookies(...);
  // const authenticity_token = ...;

  if (isAxiosDirty) {
    isAxiosDirty = false;
    myAxios = axios.create();
    myAxios.defaults.baseURL = ...;
    myAxios.defaults.headers.common.Authorization = ...;
  }

  return (
    <AxiosContext.Provider value={myAxios}>
      {props.children}
    </AxiosContext.Provider>
  );
}

export default AxiosProvider;
```

上記のように書くと、App 以下の好きなコンポーネント内で axios を参照することができます。

```tsx:src/components/MyComponent.tsx
import AxiosContext from './AxiosContext';
import { useEffect, useContext } from 'react';

function MyComponent(): JSX.Element {
  // myAxios に変更が生じるとコンポーネントが再レンダリングされる
  const myAxios = useContext(AxiosContext);

  useEffect(() => {
    myAxios.get(...).then(...); // myAxios を使った処理
  }, [myAxios]);
}
```

すでに述べた通り、useContext を使用する最大のメリットは、axios の値に変更があった場合にそれを検知してコンポーネントを再レンダリングすることができることです。

## 4. 非同期で、または useEffect 内で初期化する

さて、ここまでの実装で大抵の場合は問題ないでしょう。しかし、何らかの非同期処理の結果に応じて axios を初期化する必要があったり、または何らかの理由で初期化処理を useEffect で囲む必要があったとします。頭が痛くなってきました。

```tsx:src/components/AxiosProvider.tsx
let myAxios = axios.create();

function AxiosProvider(props: { children: JSX.Element }): JSX.Element {
  ...

  // 例えば useEffect で囲む必要があったり
  useEffect(() => {
    // または非同期で処理する必要があるかもしれない
    someProcess().then((someData) => {
      myAxios = axios.create();
      myAxios.defaults.baseURL = ...;
      myAxios.defaults.headers.common.Authorization = ...;
    });
  }, [someData]);

  return ...;
}
```

この場合でも、先程と同様に、useContext を使用することで値の変更を検知して関数コンポーネントを再レンダリングし、その上で useEffect を使用して API 呼び出しを再実行することができます。

```tsx:src/components/MyComponent.tsx
import AxiosContext from './AxiosContext';

function MyComponent(): JSX.Element {
  // axios の値が更新された場合にも MyComponent() が呼び出されるようになる
  const axios = useContext(AxiosContext);

  // axios の値が変わっていた場合のみ、API 呼び出しを実行する
  useEffect(() => {
    // axios を使った API 呼び出し
    axios.get(...).then(...);
  }, [axios]);
}
```

しかし、この書き方では axios が正常に初期化される前に一度 useEffect が実行されることになります。設定の内容によってはランタイムエラーが発生してしまう可能性があるでしょう。そこで axios を null で初期化する方法を考えます。

```diff_tsx:src/components/AxiosProvider.tsx
+ import type { AxiosStatic } from 'axios';
+
- let myAxios = axios.create();
+ let myAxios: AxiosStatic | null = null;

  function AxiosProvider(props: { children: JSX.Element }): JSX.Element {
    ...
  }
```

```diff_tsx:src/components/MyComponent.tsx
  function MyComponent(): JSX.Element {
    const axios = useContext(AxiosContext);

    useEffect(() => {
-     axios.get(...).then(...);
+     if (axios !== null) {
+       axios.get(...).then(...);
+     }
    }, [axios]);
  }
```

この書き方はかなりベストに近いですが、if 文が若干冗長に感じます。

## 5. AxiosMock の導入と if 文の回避

if 文を避けるために、AxiosMock を導入します。これは axios が初期化されていない場合のモックオブジェクトとして動作します。

```tsx:src/components/AxiosMock.tsx
class AxiosMock {
  get(url: string): AxiosMock {
    return this;
  }

  post(url: string): AxiosMock {
    return this;
  }

  delete(url: string): AxiosMock {
    return this;
  }

  patch(url: string): AxiosMock {
    return this;
  }

  then(callback: (res: { data: any }) => void): AxiosMock {
    return this;
  }

  catch(callback: (error: any) => void): AxiosMock {
    return this;
  }
}

export default AxiosMock;
```

```diff_tsx:src/components/AxiosProvider.tsx
- let myAxios: AxiosStatic | null = null;
+ let myAxios: AxiosStatic | AxiosMock = new AxiosMock();

  function AxiosProvider(props: { children: JSX.Element }): JSX.Element {
    ...
  }
```

これにより null チェックが不要になりました。

```tsx:src/components/MyComponent.tsx
function MyComponent(): JSX.Element {
  const axios = useContext(AxiosContext);

  useEffect(() => {
    axios.get(...).then(...);
  }, [axios]);
}
```

以上です、お疲れ様でした！

# おわりに

こんな感じで axios の設定を行うことができました。
React 初心者なので、他にももっと良い書き方があるという方が居たら気軽にコメントください。