Suspense は、中のコンポーネントの準備がまだできていない間、読み込み中のメッセージなどのフォールバックを表示します。待つ対象として最もよくあるのが React.lazy です。コンポーネントのコードを最初に描画されたときにだけ読み込むので、最初のダウンロードを小さく保てます。
Show chartをクリックしてください。1秒間 Loading chart... が表示され、その後チャートが表示されます。隠してからもう一度表示すると、新しいログは出ずにすぐ表示されます。lazy が読み込んだモジュールを保っているからです。
lazyでコード分割する
エディタではすべてが1つのファイルにあるので、例では遅いモジュールを手で作っています。1秒後に default エクスポートを持つオブジェクトに解決するPromiseです。実際のアプリでは、コンポーネントは独自のファイルにあり、動的インポートを渡します。
import { lazy, Suspense } from 'react';
const Chart = lazy(() => import('./Chart.jsx'));
export default function Dashboard() {
return (
<Suspense fallback={<p>Loading chart...</p>}>
<Chart />
</Suspense>
);
}
import('./Chart.jsx') はモジュールのPromiseを返します。Viteやwebpackのようなバンドラーはこの動的インポートを見つけ、Chart.jsx と、それだけが使うすべてのものを別のファイルに入れ、<Chart /> が最初に描画されたときにダウンロードします。知っておくべきルールは次のとおりです。
- モジュールにはデフォルトエクスポートが必要。
lazyはPromiseが解決したもののdefaultプロパティを読みます。名前付きエクスポートなら、対応させてください:lazy(() => import('./charts.js').then((m) => ({ default: m.LineChart })))。 lazyはモジュールのトップレベルで呼ぶ。コンポーネントの中だと、レンダリングのたびに新しいコンポーネントの型が作られるので、Reactは古いものをアンマウントし、そのstateを失い、また読み込むことになります。- ユーザーがどのみち待つ場所で分割する。ルート、モーダル、めったに開かないパネル、重いウィジェット(エディタ、チャート、地図)がよい候補です。小さなコンポーネントをすべて分割しても、リクエストと読み込み中の状態が増えるだけで得るものはありません。
クリックの前に先読みする
lazyコンポーネントは最初に描画されたときに読み込みを始めるので、ユーザーはクリックの後に少なくとも1回のダウンロードを必ず待つことになります。クリックが来ると予想できるなら、ダウンロードを早めに始めましょう。インポート関数を変数に入れておき、ホバーやフォーカスで呼びます。ブラウザはモジュールを保つので、後で lazy が同じインポートを呼んだときは、2回目のダウンロードなしに解決します。
const loadChart = () => import('./Chart.jsx');
const Chart = lazy(loadChart);
<button onMouseEnter={loadChart} onFocus={loadChart} onClick={() => setShow(true)}>
Show chart
</button>
Suspenseが何を表示するかを決める仕組み
<Suspense> の中のコンポーネントの準備ができていないとき、そのコンポーネントはサスペンドします。Reactはその部分のレンダリングを止め、その上にある最も近い Suspense の fallback を表示します。フォールバックに置き換えられるのは、待っているコンポーネントだけではなく、その境界の中のすべてです。待っていたものの準備ができると、Reactは中身をもう一度レンダリングし、フォールバックと置き換えます。
そのため、境界の位置は設計上の判断になります。独立した部分はそれぞれの境界に入れておけば、準備ができたものから表示できます。
最初はページ全体に Loading page... が表示されます。Header が外側の境界に属しているからです。ヘッダーの準備ができると、本文がその下の Loading comments... と一緒に表示され、最後にコメントが届きます。1500 を 3000 に変えると、プレビューが再読み込みされ、コメントだけの待ち時間が長くなります。内側の <Suspense> を削除する(<Comments /> は残す)と、ページはコメントを待ってから何かを表示するようになります。
React 19のuse()とSuspense
React 19では、コンポーネントが use(promise) でPromiseを読めます。Promiseがまだ保留中なら、コンポーネントはサスペンドし、最も近い Suspense がフォールバックを表示します。解決すると、use がその値を返します。詳しくはuseフックのページで扱っています。下の偽の fetchUser は本物のリクエストの代わりです。
ユーザーを順にクリックしてからUser 1に戻ってください。すぐに表示され、コンソールには新しい取得は記録されません。Promiseがすでにキャッシュにあるからです。
Promiseをキャッシュしなければならない理由
ここでの cache マップは最適化ではなく、必須です。サスペンドしたコンポーネントは、その試みから何も保ちません。Promiseが解決すると、Reactはそれを最初からもう一度レンダリングします。Profile が直接 fetchUser(id) を呼ぶと、試みのたびに新しいPromiseが作られ、新しいリクエストが始まり、またそれでサスペンドします。プロフィールは決して表示されず、コンソールは fetching user の行で埋まります。そのため、Promiseはレンダリングより長く生き残る場所から来る必要があります。
- 上のマップのような、リクエストをキーにしたキャッシュ(TanStack Queryなどのデータライブラリやフレームワークのローダーがこれを代わりにやってくれます)
- イベントハンドラやServer ComponentでPromiseを1回だけ作り、propとして下へ渡す親
新しいユーザーを選ぶたびに、やはりプロフィールがフォールバックに置き換わることに注意してください。次のユーザーの準備ができるまで前のユーザーを画面に残したいなら、更新をトランジションで包みます:startTransition(() => setId(n))。Reactはトランジションのために、すでに表示されている中身を隠しません(useTransitionを参照)。
エラーにはエラーバウンダリが必要
Suspenseが扱うのは待つことで、失敗ではありません。lazyのインポートが失敗したり(ユーザーがオフラインになった、新しいデプロイで古いチャンクが消えた)、use に渡したPromiseがrejectされたりすると、Reactはエラーを最も近いエラーバウンダリに投げます。エラーバウンダリがなければ、ルートより下のツリー全体がアンマウントされます。エラーバウンダリは今もクラスコンポーネントです(エラーバウンダリを参照)。
<ErrorBoundary fallback={<p>Could not load the chart.</p>}>
<Suspense fallback={<p>Loading chart...</p>}>
<Chart />
</Suspense>
</ErrorBoundary>
Suspenseが検知しないもの
Suspenseが反応するのは、サスペンドするコンポーネントだけです。lazy コンポーネント、use(promise)、そしてSuspense向けに作られたデータの取得元(フレームワークのローダー、Suspense対応のライブラリ)です。完了したときにstateをセットする useEffect の中のfetchはサスペンドしないので、その周りの Suspense 境界がフォールバックを表示することはありません。そのパターンでは、データ取得のページで紹介しているように、自分で loading のstateを持ちます。
よくある質問
React Suspenseとは何ですか?
<Suspense fallback={...}> は、中のどれかのコンポーネントが遅延読み込みのコードや use で読むデータなどを待っている間、その fallback を表示するコンポーネントです。中のすべての準備ができると、Reactはフォールバックを中身に差し替えます。
React.lazyは何をするものですか?
lazy(() => import('./Chart.jsx')) は、最初に描画されたときにコードがダウンロードされるコンポーネントを作ります。バンドラーはそのファイルを別のチャンクに入れるので、最初のページで読み込むJavaScriptが減ります。
Suspenseはデータ取得にも使えますか?
データの取得元が対応していれば使えます。React 19では、コンポーネントが use(promise) でPromiseを読み、解決するまでサスペンドできます。Next.jsなどのフレームワークも、自分のデータ読み込みにSuspenseを組み込んでいます。useEffect の中のfetchはSuspenseを起こしません。
Suspenseでエラーはどう扱いますか?
Suspenseが扱うのは待つことだけです。lazyのインポートやPromiseが失敗すると、エラーは最も近いエラーバウンダリに送られるので、Suspense境界(またはその親)をエラーバウンダリで包んでください。
lazyはどこで呼ぶべきですか?
モジュールのトップレベル、つまりどのコンポーネントの外でも呼んでください。コンポーネントの中で lazy を呼ぶと、レンダリングのたびに新しいコンポーネントの型が作られ、そのstateがリセットされて読み込み直されます。