T-CREATOR

React Suspenseを使う際に避けたいアンチパターン5選と解決策について紹介

React Suspenseを使う際に避けたいアンチパターン5選と解決策について紹介

React 18以降で正式サポートされたSuspenseは、非同期処理とUIの分離を簡潔に行える強力な仕組みです。

しかし、その使い方を誤ると、UXの劣化や保守性の低下、パフォーマンスの問題を引き起こすこともあります。
本記事では、React Suspenseの導入時に避けるべき代表的なアンチパターンを5つ厳選し、具体的なコード例とともにわかりやすく解説いたします。

Suspenseの基本がわかっている方はもちろん、これから導入を検討している方にも役立つ内容です。

初期読み込みにSuspenseを多用しすぎる設計

非同期UIの入り口として便利なSuspenseですが、最初の表示において多用しすぎるとUXが崩壊する恐れがあります。

問題点:ローディング地獄の発生

tsx<Suspense fallback={<Spinner />}>
  <UserProfile />
</Suspense>
<Suspense fallback={<Spinner />}>
  <UserPosts />
</Suspense>
<Suspense fallback={<Spinner />}>
  <UserFriends />
</Suspense>

一見スマートに見えるこの構成ですが、個別にデータフェッチが走り、それぞれバラバラにローディングスピナーが表示されます。

この結果、以下のような問題が起こります。

問題内容
ローディングの乱立UIにスピナーが複数同時に表示され、視認性が悪化
初期表示の遅延最も遅いコンポーネントが描画完了するまで待たされる
一貫性の欠如一部だけ表示・一部だけローディングで体験がブレる

解決策:大枠を1つのSuspenseでラップする

tsx<Suspense fallback={<PageSkeleton />}>
  <UserPage />
</Suspense>

補足:公式ドキュメントより

fetch関数を直接使ってリソース管理する

Suspenseの大前提として、「リソースがthrowするPromiseを返す」必要があります。
これを知らずに普通のfetchをそのまま使うと、うまく動作しません。

アンチパターン:Promiseの扱いを明示せずfetchする

tsxconst User = () => {
  const data = fetch('/api/user') // これはSuspenseで待ってくれない
    .then(res => res.json());

  return <div>{data.name}</div>; // dataはPromiseのまま
};

これはただの非同期処理です。Suspenseは関知しません。

解決策:リソースをラップするヘルパー関数を作成する

tsfunction wrapPromise<T>(promise: Promise<T>) {
  let status = 'pending';
  let result: T;
  let suspender = promise.then(
    r => {
      status = 'success';
      result = r;
    },
    e => {
      status = 'error';
      result = e;
    }
  );

  return {
    read() {
      if (status === 'pending') throw suspender;
      if (status === 'error') throw result;
      return result;
    },
  };
}
tsxconst resource = wrapPromise(fetch('/api/user').then(res => res.json()));

function User() {
  const user = resource.read(); // throwされるのでSuspenseで待てる
  return <div>{user.name}</div>;
}

これで初めて、Suspenseに対応した「遅延読み込み」の恩恵が得られます。

エラーハンドリングをSuspenseと混同する

Suspenseは「読み込み待ちの状態」を処理しますが、「エラー状態」は扱いません。
これを混同してしまうと、アプリが意図しない挙動を示します。

アンチパターン:Suspenseでエラーも扱えると誤解

tsx<Suspense fallback={<Loading />}>
  <ComponentThatMightThrow />
</Suspense>

このままでは、非同期処理が失敗して例外が発生しても、何も処理されずクラッシュします。

解決策:ErrorBoundaryを併用する

tsx<ErrorBoundary fallback={<ErrorUI />}>
  <Suspense fallback={<Loading />}>
    <ComponentThatMightThrow />
  </Suspense>
</ErrorBoundary>

ErrorBoundaryを使うことで、エラー発生時もUIで対処できるようになります。

公式リンク:

前半では、Suspense利用時に気をつけるべき3つのアンチパターンを紹介しました。

後半では、実際の開発現場でも特によく見かける以下の2点に焦点を当てて解説を進めてまいります。

  • Suspenseのネスト地獄による保守性の低下
  • データ取得ライブラリとの併用ミス(SWRやReact Queryとの誤用)

これらの問題に適切に対処することで、コードの可読性・拡張性・パフォーマンスが飛躍的に向上いたします。

Suspenseのネスト地獄による保守性の低下

一見問題なさそうに見えるSuspenseのネストも、数が増えると管理不能なスパゲッティコードになりがちです。

アンチパターン:コンポーネントごとにSuspenseを入れすぎる

tsx<Suspense fallback={<LoadingA />}>
  <ComponentA />
  <Suspense fallback={<LoadingB />}>
    <ComponentB />
    <Suspense fallback={<LoadingC />}>
      <ComponentC />
    </Suspense>
  </Suspense>
</Suspense>

このような構成は、一見効率的に見えて、以下のような問題を引き起こします。

問題点内容
保守性の悪化fallbackやErrorBoundaryの管理が複雑になる
UIの一貫性欠如ネストが深くなるとUXが破綻しやすい
状態追跡の難化どの層で何がロード中なのか判別困難

解決策:UI単位で適切にグループ化

まずは大きな単位でローディング状態を包み、必要に応じて粒度を調整する方法がおすすめです。

tsx<Suspense fallback={<GlobalSkeleton />}>
  <DashboardContent />
</Suspense>

さらに、個別にどうしても待機が必要な箇所のみ、ローカルなSuspenseを追加します。

tsxfunction DashboardContent() {
  return (
    <>
      <Profile />
      <Suspense fallback={<ChartSkeleton />}>
        <AnalyticsChart />
      </Suspense>
    </>
  );
}

必要最小限のSuspense構成にすることで、管理が格段に楽になります。

データ取得ライブラリとの併用ミス(SWR / React Query)

React SuspenseとSWR、React Queryなどのクライアントサイドデータ取得ライブラリは非常に相性が良いです。

しかし、設定を誤るとSuspenseが機能しなかったり、予期せぬ挙動が発生することがあります。

アンチパターン:React QueryでSuspenseを有効にし忘れる

tsxconst { data } = useQuery(['user'], fetchUser);
// Suspenseが効かず、undefinedで動く

この場合、useQueryは非同期読み込みの進行中でもundefinedを返すため、Suspenseはトリガーされません。

解決策:suspense: trueを設定する

tsxconst { data } = useQuery(['user'], fetchUser, { suspense: true });

これでuseQueryが「データ未取得状態ではPromiseをthrowする」ようになり、Suspenseのfallbackが機能します。

同様に、SWRでも以下のように対応可能です。

tsxconst { data } = useSWR('/api/user', fetcher, { suspense: true });

公式リンク:

最後に:Suspenseを使いこなすために意識すべきポイントまとめ

以下に、今回紹介した5つのアンチパターンとそれに対するベストプラクティスをまとめます。

アンチパターン問題推奨される対応策
初期読み込みにSuspenseを多用UX崩壊、スピナー乱立1つの大枠でラップし、統一感を持たせる
fetchを直接使用Suspenseが効かないPromiseラッパーを使ってread()で取得
エラーとSuspense混同アプリクラッシュErrorBoundaryを必ず併用
Suspenseのネスト地獄保守性低下、UI破綻粒度を見直し、最小構成でまとめる
SWR/React Query設定漏れSuspenseが動作しないsuspense: trueを明示的に指定

Suspenseは、適切に設計すれば非同期UIを劇的に改善できるツールです。

ですが、Reactチームも明言しているように、「Suspenseは魔法ではなく、構造の上に成り立つツール」です。
その使いどころと設計方針を明確にし、必要な場面にだけ限定的に使うことで、プロダクト全体の品質が向上いたします。

参考リンク

今後もReactの非同期レンダリングに関するベストプラクティスを発信していきます。
本記事が、ReactのUI設計における指針となれば幸いです。

記事Article

もっと見る