react-suspense について
React 開発者向けに、Suspense を使ったローディングや遅延コード分割を説明するリファレンスです。境界の設計、エラー処理、段階的な表示、よくある問題を扱います。
主な機能
- 01コンポーネントの遅延読み込み
- 02Suspense 境界のネスト
- 03エラー境界での処理
- 04段階的なコンテンツ表示
- 05スケルトンローディング
活用シーン
- 01ダッシュボードと設定画面のコード分割
- 02非同期ユーザープロフィールの読み込み
- 03画面コンテンツの段階的な表示
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ claude-dev-suite/ claude-dev-suite --skill react-suspenseインストール後、Claude Code で/react-suspenseを入力して読み込まれたか確認します。
同梱ファイルadvanced.md
詳細
React Suspense
完全なリファレンス:ストリーミング SSR、SuspenseList、Suspense 対応カスタムフック、画像の読み込み、ルートベースのコード分割、テストのパターンについては、advanced.md を参照してください。
このスキルを使用しない場合
- React 17 以前を使用している場合(サポートが限定的です)
- クラスコンポーネントを使用している場合
- React 以外のアプリケーションを構築している場合
- すべてのデータが静的な場合(非同期処理がありません)
基本概念
Suspense を使うと、非同期処理の完了を待つ間の読み込み状態を宣言的に指定できます。
import { Suspense } from 'react';
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<AsyncComponent />
</Suspense>
);
}
React.lazy によるコード分割
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));
function App() {
const [view, setView] = useState('dashboard');
return (
<div>
<nav>
<button onClick={() => setView('dashboard')}>Dashboard</button>
<button onClick={() => setView('settings')}>Settings</button>
</nav>
<Suspense fallback={<PageSkeleton />}>
{view === 'dashboard' && <Dashboard />}
{view === 'settings' && <Settings />}
</Suspense>
</div>
);
}
コンポーネントのプリロード
const Dashboard = lazy(() => import('./Dashboard'));
const preloadDashboard = () => import('./Dashboard');
function NavLink() {
return (
<Link
to="/dashboard"
onMouseEnter={preloadDashboard}
onFocus={preloadDashboard}
>
Dashboard
</Link>
);
}
Suspense によるデータ取得
TanStack Query の使用
import { useSuspenseQuery } from '@tanstack/react-query';
function UserProfile({ userId }: { userId: string }) {
const { data: user } = useSuspenseQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
return <h1>{user.name}</h1>;
}
function UserPage({ userId }: { userId: string }) {
return (
<Suspense fallback={<UserSkeleton />}>
<UserProfile userId={userId} />
</Suspense>
);
}
React 19 の use() フックの使用
import { use, Suspense } from 'react';
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise);
return <h1>{user.name}</h1>;
}
function UserPage({ userId }: { userId: string }) {
const [userPromise] = useState(() => fetchUser(userId));
return (
<Suspense fallback={<UserSkeleton />}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
ネストされた Suspense 境界
function Dashboard() {
return (
<div className="dashboard">
<Suspense fallback={<HeaderSkeleton />}>
<Header />
</Suspense>
<main>
<Suspense fallback={<StatsSkeleton />}>
<Stats />
</Suspense>
<Suspense fallback={<ChartsSkeleton />}>
<Charts />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<DataTable />
</Suspense>
</main>
</div>
);
}
Suspense でのエラーバウンダリ
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div>
<h2>Something went wrong</h2>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
// Reusable wrapper
function AsyncBoundary({
children,
fallback,
errorFallback,
}: {
children: React.ReactNode;
fallback: React.ReactNode;
errorFallback: React.ComponentType<FallbackProps>;
}) {
return (
<ErrorBoundary FallbackComponent={errorFallback}>
<Suspense fallback={fallback}>{children}</Suspense>
</ErrorBoundary>
);
}
// Usage
<AsyncBoundary
fallback={<LoadingSpinner />}
errorFallback={ErrorFallback}
>
<AsyncComponent />
</AsyncBoundary>
段階的な読み込み
function ArticlePage({ articleId }: { articleId: string }) {
return (
<article>
{/* Critical content loads first */}
<Suspense fallback={<TitleSkeleton />}>
<ArticleTitle articleId={articleId} />
</Suspense>
{/* Content loads next */}
<Suspense fallback={<ContentSkeleton />}>
<ArticleContent articleId={articleId} />
</Suspense>
{/* Less critical - loads last */}
<Suspense fallback={<CommentsSkeleton />}>
<Comments articleId={articleId} />
</Suspense>
</article>
);
}
更新時のトランジション
import { useState, useTransition, Suspense } from 'react';
function TabContainer() {
const [tab, setTab] = useState('about');
const [isPending, startTransition] = useTransition();
function selectTab(nextTab: string) {
startTransition(() => setTab(nextTab));
}
return (
<>
<TabButtons selectedTab={tab} onSelect={selectTab} />
<div className={isPending ? 'opacity-50' : ''}>
<Suspense fallback={<TabSkeleton />}>
{tab === 'about' && <About />}
{tab === 'posts' && <Posts />}
</Suspense>
</div>
</>
);
}
スケルトン読み込みのパターン
function UserCardSkeleton() {
return (
<div className="user-card">
<div className="skeleton skeleton-avatar" />
<div className="skeleton skeleton-text" style={{ width: '60%' }} />
<div className="skeleton skeleton-text" style={{ width: '40%' }} />
</div>
);
}
// CSS
const skeletonStyles = `
.skeleton {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s infinite;
border-radius: 4px;
}
@keyframes skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
`;
アンチパターン
| アンチパターン | 問題点 | 適切な方法 |
|---|---|---|
| レンダリング中に Promise を作成する | レンダリングのたびに新しい Promise が作成されます | コンポーネントの外で作成します |
| Suspense 境界が多すぎる | 読み込み状態が細分化されすぎます | 関連するコンテンツをまとめます |
| Suspense 境界が少なすぎる | アプリ全体がサスペンドします | セクションごとに境界を追加します |
| ErrorBoundary がない | エラーによってアプリがクラッシュします | Suspense を ErrorBoundary で囲みます |
| 汎用的な読み込みスピナー | UX が低下します | スケルトンローダーを使用します |
トラブルシューティング
| 問題 | 解決策 |
|---|---|
| 無限にサスペンドする | Promise の作成をコンポーネントの外に移動します |
| 読み込み状態が一瞬表示される | フォールバックの表示前に遅延を追加します |
| ウォーターフォール読み込み | データを並行して取得します |
| スクロール位置が失われる | 同じ寸法のスケルトンを使用します |
| エラーが捕捉されない | ErrorBoundary のラッパーを追加します |
ベストプラクティス
- ✅ 意味のある UI 境界に Suspense を配置します
- ✅ コンテンツと寸法を合わせたスケルトンローダーを使用します
- ✅ ErrorBoundary と組み合わせて、エラーを完全に処理します
- ✅ 緊急性の低い更新にはトランジションを使用します
- ✅ ユーザーの操作意図(ホバー、フォーカス)に応じてコンポーネントをプリロードします
- ❌ コンポーネント内で Promise を作成しないでください
- ❌ 細かすぎる境界を多数使用しないでください
リファレンスドキュメント
コメント
Markdown 対応