mcpField
R
react-suspense

react-suspense

github.com/claude-dev-suite開発4639 回インストール2 日前に更新
リポジトリ

react-suspense について

React 開発者向けに、Suspense を使ったローディングや遅延コード分割を説明するリファレンスです。境界の設計、エラー処理、段階的な表示、よくある問題を扱います。

主な機能

  1. 01コンポーネントの遅延読み込み
  2. 02Suspense 境界のネスト
  3. 03エラー境界での処理
  4. 04段階的なコンテンツ表示
  5. 05スケルトンローディング

活用シーン

  1. 01ダッシュボードと設定画面のコード分割
  2. 02非同期ユーザープロフィールの読み込み
  3. 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 を作成しないでください
  • ❌ 細かすぎる境界を多数使用しないでください

リファレンスドキュメント

コメント