1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-04-07

はじめに

以前、useEffectSuspenseを使って失敗したので、そのリベンジで今回はライブラリ使わずにuseSuspense使ってSupabaseからデータ表示するぞ!とチャレンジしてみたらだいぶ苦戦したので、ここに記録しておきます。

事象

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

関連コード

Supabaseからのデータ取得
/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;
}
App.tsx
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;
/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>
  );
};

対処

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

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

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

/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 };
};
/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>
  );
};

まとめ

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

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?