React SuspenseでUIを設計する際に避けたいアンチパターンと解決策

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 公式 Suspense ドキュメント
- React 公式 ErrorBoundary ドキュメント
- TanStack React Query Suspense対応ガイド
- SWR公式 Suspense
今後もReactの非同期レンダリングに関するベストプラクティスを発信していきます。
本記事が、ReactのUI設計における指針となれば幸いです。
Reactの記事React
- article
React × Suspenseを組み合わせてスケーラブルな非同期UIを実現する方法
- article
React Suspense × Server Componentsを使って実現するクライアントとサーバの責務分離
- article
ReactのSuspense × useTransitionを使って滑らかなUXを実現する方法
- article
React Suspenseでデータフェッチ!fetchでは動かない理由と正しい使い方
- article
React Suspenseの進化とあっと驚く並列レンダリング時代の新常識
- article
Suspense × lazyで始めるコード分割とReactアプリの最適化
- article
TypeScript 5.8 で強化された型推論!その裏で潜む 落とし穴と回避策
- article
【早見表】TypeScript Generics(ジェネリクス)の使用例と記法まとめ
- article
開発AIエディタ比較 Github Copilot vs Cursor vs Cline vs devin!それぞれの特徴や料金の違いを比較してみた
- article
【2025年5月版 早見表】TypeScript 5.7 tsconfig.jsonの主要オプションのまとめ
- article
【対処法】Cursorで発生する「Connection failed. If the problem persists ...」エラーの原因と対応
- article
Next.jsとEdge Runtimeを組み合わせて超低遅延サーバーレス表示を実現する方法