Reactだけでデータを取得するための3つの方法

## 通常
コンポーネントに必要なデータを取得する際は`useEffect`を用いて記述します。
```ts
const useData = <T>(url: string): T | null => {
  const [data, setData] = useState<T | null>(null);

  useEffect(() => {
    let ignore = false;

    fetch(url, { signal: abortController.signal })
      .then((res) => res.json())
      .then((data) => {
        if (!ignore) {
          setData(data);
        }
      })
      .catch(reportError);

    return () => {
      ignore = true;
    };
  }, [url]);

  return data;
};
```
もしくは
```ts
const useData = <T>(url: string): T | null => {
  const [data, setData] = useState<T | null>(null);

  useEffect(() => {
    const abortController = new AbortController();

    fetch(url, { signal: abortController.signal })
      .then((res) => res.json())
      .then((data) => setData(data))
      .catch((error) => {
        if (error.name === 'AbortError') return;
        reportError(error);
      });

    return () => {
      abortController.abort();
    };
  }, [url]);

  return data;
};
```
記述の違いは競合状態の解決方法になります。その記述がない場合は以下のようになります。
```ts
const useData = <T>(url: string): T | null => {
  const [data, setData] = useState<T | null>(null);

  useEffect(() => {
    fetch(url)
      .then((res) => res.json())
      .then((data) => setData(data))
      .catch(reportError);
  }, [url]);

  return data;
};
```
初期マウント時と`url`が変更されたタイミングの描画後に、`useEffect`に記述した内容が実行されて、`fetch`で獲得した情報を`data`に詰め込んでいます。
`data`は`useData`の返り値でデータの取得前は`null`を返して、取得して詰め込まれた後は`T`を返すようになっています。

このままでは、`fetch`が完了する前に次の`useEffect`が呼び出される場合に、前に呼び出した`fetch`よりも後に呼び出された`fetch`が早く帰ってきて、`data`に古い`fetch`の結果が詰め込まれるので注意する必要があります。最初に紹介したコードはその対策がされていました。

1つ目のコードはクリーンアップ時に`useEffect`内のローカル変数`ignore`を`true`にすることで、前に実行された`fetch`の結果が`data`へ詰め込まれないようにしました。

2つ目のコードは`AbortController`を用いて`fetch`に`signal`を渡して、クリーンアップ時に`AbortController`を`abort`するようにしました。これによって前に実行された`fetch`の結果が`AbortError`となって`fetch`がキャンセルされて、`data`への更新が行われなくなります。

2つ目のコードは`fetch`自体をキャンセルするので無駄なリクエストが続かない点でメリットがありますが、IEへの対応が必要な場合は`AbortController`が使えないので注意する必要があります。

## `Suspense`、`ErrorBoundary`を利用する
`Suspense`や`ErrorBoundary`を使ってコードに境界を設けている場合はもう少しシンプルに書けます。
こちら条件ではフレームワークやライブラリを用いてデータの取得を行うことが多く、スタンダードな方法がないので、あくまで私が良いと考えている方法での実装となります。
```ts
const cacheMap = new Map<string, unknown>();

const useData = <T>(url: string): T => {
  const cacheData = cacheMap.get(url) as T | undefined;
  if (cacheData === undefined) {
    throw fetch(url)
      .then((res) => res.json())
      .then((data) => cacheMap.set(url, data));
  }

  return cacheData;
};
```
`Suspense`境界で捕捉されるのは`throw`された`Promise`オブジェクトなので、`fetch`を投げる用に記述しています。`cacheMap`を作成して結果をキャッシュさせているのは、再レンダリングが発火することによる`useData`の再計算で`fetch`が再度投げられるのを防ぐためです。
このキャッシュ部分は、ReactのCanaryバージョンの世界では`cache`を使ったり、フレームワークやライブラリでは`stale-while-revalidate`が用いられてたり、より良いデータ取得のために作り込まれることが多いです。
これは以下のように利用します。
```tsx
const Sample = () => {
  const data = useData(url);

  return <Data data={data} />;
}
```

利用する際は最低でもルートで以下のように境界付けを行うようにしてください。
```tsx
<ErrorBoundary>
  <Suspense>
    <Sample />
  </Suspense>
</ErrorBoundary>
```


## canaryバージョン
Reactの[Canary](https://ja.react.dev/community/versioning-policy#canary-channel)リリースされている`use`を用いることで自作することなくデータの取得を行えます。こちらも`Suspense`や`ErrorBoundary`を使ってコードに境界を設ける必要があります。
```tsx
const Sample = () => {
  const data = use(fetchData);

  return <Data data={data} />;
};
```
`use`は他のhooksとは異なり、if文中などに記述することもできますが、呼び出す場所はレンダリング時に計算される場所でなくてはいけません(イベントハンドラ内には記述できません)。