Skip to content
⚠️ This article was written in 2021. Some content may be outdated.

React Suspense データフェッチングパターン

React 18 のリリースが近づき、Suspense のデータフェッチパターンにようやく公式の推奨方式が登場しました。これまで Suspense はコード分割のローディング状態にしか使えませんでしたが、今後はデータフェッチにも使用できるようになります。

Suspense の核心的な考え方

Suspense は「loading 状態管理」ではなく、「非同期依存関係のコーディネーター」です。

jsx
// 従来の方法:コンポーネント自身が loading 状態を管理
function UserProfile({ userId }) {
  const [user, setUser] = useState(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    fetchUser(userId).then(data => {
      setUser(data)
      setLoading(false)
    })
  }, [userId])

  if (loading) return <Spinner />
  return <div>{user.name}</div>
}

// Suspense の方法:データソースが Promise を投げ、Suspense がキャッチ
function UserProfile({ userId }) {
  // read() は Promise(pending)を投げるか、データ(resolved)を返す
  const user = userResource.read(userId)
  return <div>{user.name}</div>
}

// 父组件
function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <UserProfile userId={1} />
    </Suspense>
  )
}

コンポーネントは loading 状態を気にする必要がなく、Suspense が一元的に処理します。

シンプルな Suspense データソースの実装

typescript
// utils/createResource.ts
export function createResource<T>(asyncFn: () => Promise<T>) {
  let status: 'pending' | 'success' | 'error' = 'pending'
  let result: T
  let error: Error

  const suspender = asyncFn().then(
    (data) => {
      status = 'success'
      result = data
    },
    (e) => {
      status = 'error'
      error = e
    }
  )

  return {
    read(): T {
      switch (status) {
        case 'pending':
          throw suspender  // Suspense が Promise をキャッチ
        case 'error':
          throw error      // ErrorBoundary がエラーをキャッチ
        case 'success':
          return result
      }
    },
  }
}
jsx
// 使用例
import { createResource } from './utils/createResource'

// リソースを作成(コンポーネント外部または useMemo でキャッシュ)
const userResource = createResource(() => fetch('/api/user/1').then(r => r.json()))

function UserProfile() {
  const user = userResource.read()  // Promise を投げる可能性がある
  return <div>{user.name}</div>
}

function App() {
  return (
    <ErrorBoundary fallback={<ErrorPage />}>
      <Suspense fallback={<Spinner />}>
        <UserProfile />
      </Suspense>
    </ErrorBoundary>
  )
}

React Query との組み合わせ(推奨)

React Query 3.25+ は既に Suspense モードをサポートしています:

jsx
import { useQuery } from 'react-query'

function UserProfile({ userId }) {
  const { data: user } = useQuery(
    ['user', userId],
    () => fetchUser(userId),
    {
      suspense: true,  // Suspense モードを有効化
    }
  )

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  )
}

// 布局
function UserPage({ userId }) {
  return (
    <div>
      <Suspense fallback={<ProfileSkeleton />}>
        <UserProfile userId={userId} />
      </Suspense>

      <Suspense fallback={<PostsSkeleton />}>
        <UserPosts userId={userId} />
      </Suspense>

      <Suspense fallback={<FollowersSkeleton />}>
        <Followers userId={userId} />
      </Suspense>
    </div>
  )
}

各領域が独立して読み込まれ、互いにブロックしません。

ネストされた Suspense

jsx
function App() {
  return (
    <Suspense fallback={<FullPageSkeleton />}>
      <Header />

      <main>
        <Suspense fallback={<SidebarSkeleton />}>
          <Sidebar />

          <Suspense fallback={<ContentSkeleton />}>
            <MainContent />
          </Suspense>
        </Suspense>
      </main>
    </Suspense>
  )
}

外側の Suspense はフォールバック、内側の Suspense は部分的なローディングを処理します。React 18 のストリーミング SSR は HTML をセグメントごとに送信できます。

React 18 Suspense SSR との連携

jsx
// サーバー側:選択的ハイドレーション(Selective Hydration)
import { hydrateRoot } from 'react-dom/client'

function App() {
  return (
    <html>
      <body>
        <Suspense fallback={<HeaderSkeleton />}>
          <Header />
        </Suspense>

        <Suspense fallback={<MainSkeleton />}>
          <MainContent />
        </Suspense>
      </body>
    </html>
  )
}

// サーバー側は最初に Header の HTML を送信
// MainContent のデータ準備ができたら、ストリーミングで追加送信
// クライアント側はセグメントごとに hydrate し、すべてのコンテンツを待つ必要はない

ユーザーはより早くコンテンツを確認でき、インタラクションもより早く利用可能になります。

エラー処理

jsx
import { ErrorBoundary } from 'react-error-boundary'

function App() {
  return (
    <ErrorBoundary
      fallbackRender={({ error, resetErrorBoundary }) => (
        <div>
          <p>出错了:{error.message}</p>
          <button onClick={resetErrorBoundary}>重试</button>
        </div>
      )}
    >
      <Suspense fallback={<Spinner />}>
        <UserProfile userId={1} />
      </Suspense>
    </ErrorBoundary>
  )
}

Suspense が loading を処理し、ErrorBoundary がエラーを処理します。責務が明確に分離されています。

注意事項

jsx
// ❌ Suspense 内で条件付きレンダリングのデータ取得をしない
function Bad({ showProfile }) {
  return (
    <Suspense fallback={<Spinner />}>
      {/* 条件切り替え時に予期しない Suspense が発動する可能性がある */}
      {showProfile ? <UserProfile /> : <GuestView />}
    </Suspense>
  )
}

// ✅ key を使って強制的に再作成
function Good({ showProfile, userId }) {
  return (
    <Suspense fallback={<Spinner />}>
      {showProfile
        ? <UserProfile key={userId} userId={userId} />
        : <GuestView />}
    </Suspense>
  )
}

まとめ

  • Suspense データフェッチの核心:コンポーネントが Promise を投げ、Suspense がキャッチして fallback を表示
  • React Query / SWR などのライブラリの Suspense モードを推奨、自前実装は不要
  • ネストされた Suspense で領域ごとの読み込みを実現、React 18 のストリーミング SSR との組み合わせが最適
  • Suspense + ErrorBoundary:loading とエラー処理の責務分離
  • React 18 正式版リリース後、このパターンが主流になる

MIT Licensed