ライブラリなしでSuspenseとuseを使ったらハマった

## はじめに
以前、[`useEffect`と`Suspense`を使って失敗した](https://qiita.com/IchidaU/items/444a674e3aa84ea2f3ba)ので、そのリベンジで今回はライブラリ使わずに`use`と`Suspense`使ってSupabaseからデータ表示するぞ！とチャレンジしてみたらだいぶ苦戦したので、ここに記録しておきます。

## 事象
下記のコードで`use`と`Suspense`を使用してSupabaseからのデータ表示を試みたところ、ローディング画面のまま遷移せず、コンソールを開くと無限ループが発生していました。

### 関連コード
<details><summary>Supabaseからのデータ取得</summary>

```react:/lib/record.ts
import { Record } from "../domain/record";
import { supabase } from "../utils/supabase";

export async function GetRecords(): Promise<Record[]> {
  const response = await supabase.from("study-record").select("*");
  if (response.error) {
    throw new Error(response.error.message);
  }

  const recordsData = response.data.map((record) => {
    return new Record(record.id, record.title, record.time);
  });

  console.log("recordsData", recordsData);
  return recordsData;
}
```
</details>

<details><summary>App.tsx</summary>

```react:App.tsx
import { Suspense } from "react";
import { RecordList } from "./components/RecordList";
import { ErrorBoundary } from "react-error-boundary";

function App() {
  return (
    <>
      <ErrorBoundary fallback={<div>エラーが発生しました</div>}>
        <Suspense fallback={<div>読み込み中...</div>}>
          <RecordList />
        </Suspense>
      </ErrorBoundary>
    </>
  );
}

export default App;
```

</details>

```react:/components/RecordList.tsx
import { use } from "react";
import { GetRecords } from "../lib/record";

export const RecordList = () => {
  const records = use(GetRecords());
  
  return (
    <div>
      {records.map(record => (
        <div key={record.id}>
          <h2>{record.title}</h2>
          <p>{record.time}</p>
        </div>
      ))}
    </div>
  );
};
```

## 対処
結論から言うと、`use`と`Suspense`の仕様をよくわかっていなかったのが原因でした。
`Suspense`は、`Promise`が解決するとコンポーネントを再レンダリングします。

しかし、今回のコードでは再レンダリングされる度にSupabaseからのデータ取得関数である`GetRecords()`が実行されるため、その都度新たなPromiseが作成されて無限ループが発生、いつまでも再レンダリングが完了しない、ということになっていました。

そこで、`Promise`を一度だけ作成しキャッシュするカスタムフックを作成し、それを呼び出すことで解決しました。

```react:/hooks/useFetchData.ts
import { use } from "react";
import { GetRecords } from "../lib/record";
import { Record } from "../domain/record";

let recordsPromise: Promise<Record[]> | null = null;

export const useFetchData = (): { records: Record[] } => {
  if (!recordsPromise) {
    recordsPromise = new Promise<Record[]>((resolve) => {
      (async () => {
        const data = await GetRecords();
        resolve(data);
      })();
    });
  }

  const records = use(recordsPromise);

  return { records };
};
```

```react:/components/RecordList.tsx
import { useFetchData } from "../hooks/useFetchData";

export const RecordList = () => {
  const { records } = useFetchData();
  console.log("records", records);

  return (
    <div>
      {records.map((record) => (
        <div key={record.id}>
          <h2>{record.title}</h2>
          <p>{record.time}</p>
        </div>
      ))}
    </div>
  );
};
```

## まとめ
今回は、`use`と`Suspense`の理解不足に起因するエラーでした。
解決するにあたって、`Suspense`に渡す「キャッシュ済み`Promise`」というものがどういうものかも理解しておらず、`Promise`についての理解も不足していることに気付きました。
非同期処理周りの理解をもっと深めることが必要だと痛感しました。

## 参考

https://ja.react.dev/reference/react/use

https://ja.react.dev/reference/react/Suspense