React Server ComponentsでのPromiseの最適な扱い方

React 18以降では、**React Server Components (RSC)** と呼ばれる仕組みが導入され、サーバー側でReactコンポーネントをレンダリングすることができます。RSCでデータを取得するとき、初心者の方がつまずきやすいのが**Promiseの扱い**です。特に、Promiseを単純に`await`する方法と、**`<Suspense>`コンポーネント + `use()`フック**を使う方法の違いは重要なポイントです。本記事では、なぜRSCではPromiseを`await`せずに**Suspenseと`use()`を使うべきなのか**、その理由やメリット・デメリットをわかりやすく解説します。

## なぜRSCで`await`ではなくSuspense+use()を使うのか？

結論から言えば、**React Server ComponentsではPromiseを直接`await`せずに、`<Suspense>`とReactの`use()`フックを組み合わせて扱うのがベストプラクティス**です。その最大の理由は、**サーバーサイドのストリーミングレンダリング（Streaming SSR）を活用できる**からです。 ([reactjs - Why use Suspense in React Server Components - Stack Overflow](https://stackoverflow.com/questions/78013664/why-use-suspense-in-react-server-components#:~:text=The%20greatest%20benefit%20of%20using,These%20dynamic))

- `<Suspense>`を使うと、サーバーはまずページ全体の骨組み（**シェル**）をレンダリングし、データが必要な部分を「穴あき」の状態にしておくことができます ([reactjs - Why use Suspense in React Server Components - Stack Overflow](https://stackoverflow.com/questions/78013664/why-use-suspense-in-react-server-components#:~:text=The%20greatest%20benefit%20of%20using,These%20dynamic))。その穴の部分（非同期コンポーネント部分）には、一時的にフォールバックUI（例えば「Loading...」のようなローディング表示）が表示されます。
- サーバーはシェル部分のHTMLを先にクライアントへストリーミング送信し、その後バックエンドでデータ取得（Promiseの解決）を続けます。データが取得でき次第、その部分のコンテンツを**ストリーム経由で追送信**し、フォールバックUIと置き換えてブラウザに表示させます。
- このように**段階的にHTMLを送る**ことで、ユーザーはページの一部をすぐに目にでき、残りの部分もデータが揃い次第シームレスに表示されます。結果として初期表示が早くなり、ユーザーエクスペリエンスが向上します。

一方、Promiseを普通に`await`してしまうと何が起こるでしょうか？　サーバーはPromiseが解決されるまで**レンダリング処理を一時停止**し、HTMLの生成を待ちます。その間、ユーザーには何も表示されません。つまり、**`await`を使うとそのコンポーネント以下のツリー全体のレンダリングがブロックされてしまう**のです。複数のデータ取得が連鎖すると「*waterfall*（滝のように順次実行されるため遅延が蓄積すること）」が発生し、ページ全体の表示が大きく遅れてしまいます。

まとめると、**Suspenseを使えば非同期処理中でも他の部分の表示・操作をブロックしない**ため、RSCではSuspenseを活用した方が良いのです。

## `await`した場合のデメリット

RSCでPromiseを`await`してしまうと、以下のようなデメリットがあります。

- **ページ表示の遅延**: `await`は同期的に処理を待つため、そのコンポーネントのレンダリング結果が出るまで他の部分も含めて何も送信されません。ユーザーは白紙のページや固まった状態を長く待つことになります。特に複数のデータを順番に`await`すると待ち時間がどんどん長くなります（いわゆる「データ取得の滝問題」）。
- **ストリーミング不可**: サーバー側でPromiseを解決 (`await`) してしまうと、クライアントには「完成済みの結果」しか送れません。ストリーミングSSRの恩恵（部分的な先送り表示）が受けられず、「データが全部揃ってからまとめて表示」という従来型の挙動になります。
- **UXの低下**: 画面全体がロード完了までローディング状態になるため、ユーザーはインタラクションできずストレスを感じます。場合によってはユーザーがページが動いていないと感じて離脱する可能性もあります。

例えば、次のようなコードを考えます。

```jsx
// ❌ 良くない例：awaitでデータ取得をブロックしてしまう
export default async function Page() {
  // データを取得（この間、このコンポーネント以下のレンダリングは停止）
  const res = await fetch('https://api.example.com/data');
  const data = await res.json();

  return (
    <main>
      <h1>ダッシュボード</h1>
      {/* データ取得完了後でないと以下はレンダリングされない */}
      <div>データ件数: {data.count}</div>
    </main>
  );
}
```

上記では`fetch`でデータを取得し終わるまで、`<h1>ダッシュボード</h1>`すらユーザーに表示されません。サーバーはデータが揃うまでHTMLを送出しないため、ユーザーは白い画面のまま待つことになります。

## Suspense＋`use()`を使うメリット

`<Suspense>`コンポーネントとReactの`use()`フックを組み合わせると、RSCでのデータ取得は驚くほどスムーズになります。主なメリットは次のとおりです。

- **ストリーミングによる高速表示**: 前述の通り、Suspenseは非同期コンポーネントの読み込みを待つ間フォールバックUI（`fallback`プロップで指定したコンポーネント）を表示できます。そのため、**ページの他の部分はすぐ表示して、重いデータ部分だけ遅れて差し込む**ということが可能です ([reactjs - Why use Suspense in React Server Components - Stack Overflow](https://stackoverflow.com/questions/78013664/why-use-suspense-in-react-server-components#:~:text=The%20greatest%20benefit%20of%20using,These%20dynamic))。ユーザーは瞬時にページの骨組みを見られ、後からデータ部分が埋まっていくため、体感速度が向上します。
- **レンダリングブロックの範囲縮小**: Suspenseで囲まれた部分以外はブロックされずレンダリングされます。例えばページ全体ではなく一部のウィジェットだけデータ取得が遅い場合、そのウィジェット部分だけをSuspenseで囲めば、他の部分は遅延なく表示・操作できます。
- **コードの見通し改善**: データ取得のために複雑なローディング状態管理や副作用フック (`useEffect` など) を書かずに済みます。サーバーコンポーネント内で自然に`async/await`を書く代わりに、`use()`を使ってPromiseの結果を直接参照できるため、より宣言的でシンプルなコードになります。エラーハンドリングも、Promiseがrejectした場合は自動的にエラーとして扱われ、近くのError Boundaryで捕捉できます（エラーハンドリングの詳細は本記事の範囲外ですが、Suspenseと組み合わせるとエレガントに処理できます）。
- **クライアントコンポーネントとの連携**: RSCは基本的にサーバー上でレンダリングされますが、場合によっては取得したデータをクライアントコンポーネントに渡して使いたいこともあります。`use()`フックを使えば、サーバーからクライアントコンポーネントに**Promise自体**を渡し、クライアント側でそれを解決して表示することも可能です。Reactは未解決のPromiseを検知すると内部で一時停止 (`throw`によるサスペンド) し、適切に再開・表示してくれます。

では、先ほどのコード例をSuspenseと`use()`を用いて改善してみましょう。

```jsx
import { Suspense, use } from 'react';

// ✅ 良い例：Suspenseで非同期処理中でもページ全体をブロックしない
export default function Page() {
  // データ取得を開始（Promiseを得る）。ここではまだawaitしない！
  const dataPromise = fetch('https://api.example.com/data').then(res => res.json());

  return (
    <main>
      <h1>ダッシュボード</h1>
      {/* Suspenseでラップし、フォールバックを指定 */}
      <Suspense fallback={<p>Loading...</p>}>
        {/* Promiseを子コンポーネントに渡す */}
        <DataWidget dataPromise={dataPromise} />
      </Suspense>
    </main>
  );
}

// DataWidgetはPromiseを受け取り、その完了を待ってデータを表示するコンポーネント
function DataWidget({ dataPromise }) {
  // use()でPromiseの中身を取得（Promiseが未完了ならこのコンポーネントはサスペンドする）
  const data = use(dataPromise);
  return <div>データ件数: {data.count}</div>;
}
```

上記のコードでは、`Page`コンポーネントで`dataPromise`を取得していますが**`await`していない点**に注目してください。`Page`は同期的にJSXを返すので、`<h1>ダッシュボード</h1>`など静的な部分は即座にレンダリングされます。一方で、`<Suspense>`でラップした`<DataWidget>`内では`use(dataPromise)`が呼ばれます。`dataPromise`が解決していない場合、`use()`は内部的にPromiseを`throw`し、このコンポーネントのレンダリングを一時停止させます。Reactはその`throw`されたPromiseをキャッチし、親のSuspenseがある場合は自動的にフォールバックUI (`<p>Loading...</p>`) を表示します。データ取得完了後、`dataPromise`が解決されると`DataWidget`が再レンダリングされ、本物のデータ（件数）が表示されます。

この仕組みによって、**ユーザーはページを開いてすぐ「ダッシュボード」という見出しと"Loading..."インジケーターを見ることができ、バックエンドでデータ取得が終わった瞬間に件数がパッと表示される**わけです。重い処理でなければフォールバックは一瞬で消え去るでしょうし、仮に数秒かかる場合でもユーザーには途中経過が見えるため安心感があります。

## シンプルな例で理解する

上記の例を図式化してみましょう（※コードは擬似的な流れを示すものです）:

# await vs Suspense+use の比較

## awaitを使った場合（従来の方法）
```mermaid
sequenceDiagram
    participant S as サーバー
    participant C as クライアント

    S->>S: データ取得開始
    Note right of S: 3秒経過
    S->>S: データ取得完了
    S->>S: HTML生成
    S->>C: HTML送信
    Note over C: 画面表示
    Note over C: (それまでの3秒間は<br/>画面が真っ白)
```

サーバーはデータ取得が終わるまでHTMLを送りません。ユーザーの画面は3秒間真っ白です。その後、まとめて完成したHTMLが届き、一度に表示されます。

## Suspense+useを使った場合（Streaming SSR）
```mermaid
sequenceDiagram
    participant S as サーバー
    participant C as クライアント

    S->>S: データ取得開始
    S->>C: 静的な部分のHTML送信
    Note over C: 見出しと「Loading...」表示
    S->>S: データ取得継続
    Note right of S: 3秒経過
    S->>S: データ取得完了
    S->>C: 該当部分のHTML追送信
    Note over C: 「Loading...」が<br/>本物のデータに置き換わる
```

サーバーは最初に静的なHTMLとローディング表示を送信し、その後データ取得が完了次第、動的なコンテンツを追加で送信します。ユーザーは最初から何らかの表示を見ることができ、完了したデータは自動的に反映されます。

  このように、まず**ページの土台とフォールバックUIが即座に表示**され、その後**足りない部分だけ後から埋まる**形になります。ユーザーから見れば、ページは瞬時に表示されて徐々に内容がリッチになっていくように見えるでしょう。特にネットワークが遅い場合でも、「何も起こらない…」という状態を防げるのは大きな利点です。

「Promiseを`await`しないまま使うなんて大丈夫なの？」と思うかもしれません。しかし、React 18+ではこの**「PromiseをそのままUIとして扱う」**発想がSuspense機能によって公式にサポートされています。`use()`フックのおかげで、上記の例のように**本当にawaitせずにPromiseを使える**のです。Reactが裏でちゃんと面倒を見てくれるので心配はいりません。

> **補足:** `use()`フックはReact 18で登場した実験的な機能でしたが、現在では公式にRSCなどで使用できます。`use(promise)`とすると、ReactはPromiseの状態に応じて自動で処理を切り替えてくれます（解決済みなら値を返し、未解決なら一時停止し、エラーなら投げ直す）。

## ベストプラクティスと設計のポイント

最後に、React Server Componentsにおける非同期処理のベストプラクティスをまとめます。

- **可能な限りSuspenseを活用する**: 非同期データ取得が絡むコンポーネントは、積極的に`<Suspense>`でラップしましょう。こうすることで、データロード中でも他の部分の表示を妨げず、ユーザーに素早いフィードバックを返せます。特に、大きなページ全体を一度にレンダリングするより、部分ごとにSuspense境界を設けた方がインタラクティブ性が高まります。
- **Promiseは先に作成し、あとで`use()`する**: データ取得のリクエスト自体はできるだけ早い段階（上位コンポーネント）で開始し、**そのPromiseを子コンポーネントに渡して`use()`で読む**ようにしましょう。こうすることでデータ取得が並行して行われ、無駄な待ち時間が減ります。複数のデータ源がある場合も、一つずつ順番に`await`するのではなく、Promiseを複数生成してから`Promise.all()`するか、それぞれ別々のSuspense境界で囲むことで**並行処理**できます。
- **フォールバックUIを適切に用意する**: Suspenseの`fallback`にはユーザーが違和感を持たないロード中の表示を入れましょう。単純な"Loading..."テキストやスピナーでも構いませんが、よりコンテキストに合ったプレースホルダー（例えばローディング用のスケルトンUI）を用意するとユーザー体験が向上します。フォールバックはできるだけ早く表示されるため、**ユーザーにはページが動いていることが伝わります**。
- **エラーハンドリングも忘れずに**: Promiseが失敗する場合もあります。Suspenseと組み合わせる際は、上位にError Boundary（`<ErrorBoundary>`コンポーネント）を設置しておくと安心です。`use()`で投げられたエラーはそこでキャッチできるので、エラー時にフォールバックとしてエラーメッセージを表示する、といった対応が可能です。
- **クライアントコンポーネントではデータ取得しない**: RSCの利点を活かすために、データフェッチは基本的にサーバーコンポーネント側で行いましょう。クライアントコンポーネント（`"use client"`指定したコンポーネント）内では直接非同期処理を行えず、誤ってasync/awaitを書くとエラーになります。どうしてもクライアント側でデータ取得が必要な場合は、React QueryやSWRといったライブラリの利用を検討してください。ただし初学者のうちは、まずRSC + Suspenseのパターンに慣れることをおすすめします。

以上、React Server ComponentsにおけるPromiseの扱い方について、`await`と`Suspense+use()`の比較からベストプラクティスまで解説しました。最初は少し不思議に思えるかもしれませんが、実際に手を動かしてみるとSuspenseを使った非同期処理のシンプルさと強力さに驚くでしょう。ぜひ自身のプロジェクトでも試してみて、快適なユーザー体験を提供できるRSCの非同期処理をマスターしてください。

【参考資料】

- React公式: Suspenseと`use()`フックの解説
- Next.js公式: データフェッチングパターンとStreaming SSR
- Ed Spencer: React Server ComponentsでのStreamingに関するブログ記事