ReactとSuspenseで構築する柔軟な非同期UIの設計法について紹介

現代のフロントエンドアプリケーションでは、非同期処理とその制御が極めて重要です。 特にユーザー体験を向上させるためには、ローディング、エラーハンドリング、そしてデータ取得のタイミングを慎重に設計する必要があります。 ReactのSuspenseは、こうした課題に対して非常に柔軟かつ強力なソリューションを提供します。
本記事では、ReactとSuspenseを活用した非同期UI設計について、丁寧に、かつ実践的なコード例とともに詳しく解説してまいります。
Suspenseの基本的な仕組み
Suspenseは、非同期コンポーネントのローディング状態を制御するための仕組みです。 React 18以降では、データ取得やコード分割といった非同期処理において、本格的に利用可能となっています。
基本構文とローディングUIの指定
以下に、最もシンプルなSuspenseの使い方を紹介します:
tsximport React, { Suspense } from 'react';
const LazyComponent = React.lazy(() => import('./MyComponent'));
export const App = () => {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
};
この例では、LazyComponent
が読み込まれるまで、fallback
に指定したローディングUIが表示されます。
コードスプリッティングとの連携
React.lazyは、モジュールを非同期に読み込む手段で、React公式でも推奨されています。
tsxconst Chart = React.lazy(() => import('./components/Chart'));
データ取得におけるSuspenseの活用
React 18以降、use
フックとともにSuspense
を使うことで、データ取得を待ってからUIを描画する設計が可能になりました。
非同期関数とReactのuse関数
tsx// fetchData.ts
export const fetchUser = async (id: string) => {
const res = await fetch(`/api/user/${id}`);
if (!res.ok) throw new Error('データ取得に失敗しました');
return res.json();
};
tsx// UserProfile.tsx
import { use } from 'react';
import { fetchUser } from './fetchData';
export const UserProfile = ({ userId }: { userId: string }) => {
const user = use(fetchUser(userId));
return <div>{user.name}</div>;
};
この例では、use
フックによってPromiseの解決を待機し、結果を同期的に扱えるようになっています。
サスペンド可能なリソースの自作とキャッシュ制御
Suspenseでデータ取得を完全に制御するためには、リソースのラッパー関数を作成するのが有効です。
ラッパー関数の作成
tsx// createResource.ts
export const createResource = <T>(asyncFunc: () => Promise<T>) => {
let status = 'pending';
let result: T;
let suspender = asyncFunc().then(
(r) => {
status = 'success';
result = r;
},
(e) => {
status = 'error';
result = e;
}
);
return {
read(): T {
if (status === 'pending') {
throw suspender;
} else if (status === 'error') {
throw result;
} else {
return result;
}
},
};
};
利用側のコード
tsxconst userResource = createResource(() => fetchUser('123'));
export const Profile = () => {
const user = userResource.read();
return <div>{user.name}</div>;
};
このようにすることで、リソースがまだ読み込み中であればSuspenseに制御を移譲できます。
エラーハンドリングとErrorBoundary
非同期UIで特に重要なのが、エラー時の制御です。 Suspenseと併用することで、エラー時にフォールバックUIを提供可能です。
ErrorBoundaryの実装
tsximport React from 'react';
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
console.error('エラー情報:', error, info);
}
render() {
if (this.state.hasError) {
return <div>エラーが発生しました</div>;
}
return this.props.children;
}
}
export default ErrorBoundary;
Suspenseと組み合わせる
tsx<ErrorBoundary>
<Suspense fallback={<div>読み込み中です...</div>}>
<Profile />
</Suspense>
</ErrorBoundary>
この構成により、読み込み中もエラー時も適切なUIを表示できます。
より複雑な構成と複数のSuspense
実際のアプリケーションでは、複数のデータソースを扱うケースがほとんどです。 Suspenseはネストして使うことができ、柔軟に構成を組むことが可能です。
tsx<Suspense fallback={<div>ユーザー情報を読み込み中...</div>}>
<UserProfile userId="1" />
<Suspense fallback={<div>通知を読み込み中...</div>}>
<UserNotifications userId="1" />
</Suspense>
</Suspense>
このように、個別のコンポーネント単位でローディングUIを細分化できます。
サーバーコンポーネントとの組み合わせ
Next.js 13以降では、React Server ComponentsとSuspenseの組み合わせが可能になり、初期表示の最適化が実現できます。
tsx// app/page.tsx (Next.js 13)
import { Suspense } from 'react';
import UserProfile from './UserProfile';
export default function Page() {
return (
<div>
<h1>ダッシュボード</h1>
<Suspense fallback={<div>読み込み中...</div>}>
<UserProfile userId="123" />
</Suspense>
</div>
);
}
この構成は、パフォーマンスとUXの両立を図るうえで極めて効果的です。
まとめ
Suspenseは、非同期UIの設計を大幅に簡潔かつ明快にします。
従来のuseEffect
とローカル状態によるローディング制御に比べて、より宣言的で可読性が高く、エラー処理とも自然に統合可能です。
今後はReactのコアエコシステムにおいて、さらに重要な役割を担っていくことが予想されます。
以下に本記事のポイントを整理します:
機能 | 内容 |
---|---|
Suspense | ローディングUIを制御する仕組み |
React.lazy | 動的インポートによるコード分割 |
use(fetch()) | React 18以降の非同期取得の同期的処理 |
createResource | カスタムリソースラッパーでSuspenseに対応 |
ErrorBoundary | エラーハンドリング用のコンポーネント |
ネスト | 個別のローディングUI制御に柔軟対応 |
Next.jsとの統合 | Server Componentsと組み合わせた最適化 |
今後の開発において、より洗練された非同期体験を提供するために、Suspenseを活用してみてください。
記事Article
もっと見る- article
Reactの状態管理2025:「useState」「Redux Toolkit」「Jotai」「Zustand」を比較してみた
- article
Next.jsでの画像最適化戦略:next/image vs 外部CDNを比較してみた
- article
React Server Componentsの可能性と課題を実用に向けて考えてみる
- article
Next.js 13 App Router入門:Pages Routerとの違いと移行のコツをわかりやすく紹介
- article
React開発における状態管理のスケーラブルな構造について考えてみる
- article
Next.js開発でちょいちょい発生する、ハイドレーションエラーの原因と対策を紹介