<Suspense> を使うことで、子要素が読み込みを完了するまでフォールバックを表示させることができます。

<Suspense fallback={<Loading />}>
<SomeComponent />
</Suspense>

リファレンス

<Suspense>

props

  • children: レンダーしようとしている実際の UI です。children がレンダー中にサスペンド(suspend, 一時中断)すると、サスペンスバウンダリは fallback のレンダーに切り替わります。
  • fallback: 実際の UI がまだ読み込みを完了していない場合に、その代わりにレンダーする代替 UI です。有効な React ノードであれば何でも受け付けますが、現実的には、フォールバックとは軽量なプレースホルダビュー、つまりローディングスピナやスケルトンのようなものです。children がサスペンドすると、サスペンスは自動的に fallback に切り替わり、データが準備できたら children に戻ります。fallback 自体がレンダー中にサスペンドした場合、親のサスペンスバウンダリのうち最も近いものがアクティブになります。

注意点

  • React は、初回マウントが成功するより前にサスペンドしたレンダーに関しては、一切の state を保持しません。コンポーネントが読み込まれたときに、React はサスペンドしていたツリーのレンダーを最初からやり直します。
  • すでにツリーにコンテンツを表示していたサスペンスが再度サスペンドした場合、fallback が再び表示されます。しかしその更新が startTransition または useDeferredValue によって引き起こされた場合を除きます。
  • 既に表示されているコンテンツが再度サスペンドしたために React がそれを隠す必要が生じた場合、React はコンテンツツリーのレイアウトエフェクトをクリーンアップします。コンテンツが再度表示できるようになったら、React はレイアウトエフェクトを再度実行します。これにより、DOM レイアウトを測定するエフェクトがコンテンツが隠れている間に測定を試みないようにします。
  • React には、ストリーミングサーバレンダリング選択的ハイドレーションなどの、サスペンスと統合された自動的な最適化が含まれています。詳しくは、アーキテクチャの概要テクニカルトークを参照してください。

使用法

コンテンツの読み込み中にフォールバックを表示する

アプリケーションの任意の部分をサスペンスバウンダリでラップできます。

<Suspense fallback={<Loading />}>
<Albums />
</Suspense>

React は、子要素が必要とするすべてのコードとデータが読み込まれるまで、ロード中のフォールバックを表示します。

以下の例では、Albums コンポーネントはアルバムのリストをフェッチする間、サスペンドします。レンダーの準備が整うまで、React は上にある最も近いサスペンスバウンダリを、フォールバック(Loading コンポーネント)を表示するように切り替えます。その後データが読み込まれると、React は Loading フォールバックを非表示にし、データとともに Albums コンポーネントをレンダーします。

import { Suspense } from 'react';
import Albums from './Albums.js';

export default function ArtistPage({ artist }) {
  return (
    <>
      <h1>{artist.name}</h1>
      <Suspense fallback={<Loading />}>
        <Albums artistId={artist.id} />
      </Suspense>
    </>
  );
}

function Loading() {
  return <h2>🌀 Loading...</h2>;
}

補足

サスペンスコンポーネントをアクティブ化できるのはサスペンス対応のデータソースだけです。これには以下が含まれます:

  • RelayNext.js のようなサスペンス対応のフレームワークでのデータフェッチ
  • lazy を用いたコンポーネントコードの遅延ロード
  • use を用いたキャッシュ済みプロミス (Promise) からの値の読み取り

サスペンスはエフェクトやイベントハンドラ内でデータフェッチが行われた場合にはそれを検出しません

上記の Albums コンポーネントで実際にデータをロードする方法は、使用するフレームワークによって異なります。サスペンス対応のフレームワークを使用している場合、詳細はデータフェッチに関するドキュメンテーション内に記載されているはずです。

使い方の規約のある (opinionated) フレームワークを使用せずにサスペンスを使ったデータフェッチを行うことは、まだサポートされていません。サスペンス対応のデータソースを実装するための要件はまだ不安定であり、ドキュメント化されていません。データソースをサスペンスと統合するための公式な API は、React の将来のバージョンでリリースされる予定です。


コンテンツを一度にまとめて表示する

デフォルトでは、サスペンス内のすべてのツリーはひとつの単位として扱われます。例えば、以下のコンポーネントのうちどれかひとつでもデータ待ちでサスペンドしていれば、すべてがまとめてローディングインジケータに置き換わります。

<Suspense fallback={<Loading />}>
<Biography />
<Panel>
<Albums />
</Panel>
</Suspense>

その後、すべてが表示可能になった時点で、一斉に表示されます。

以下の例では、BiographyAlbums の両方がデータをフェッチしています。しかし、単一のサスペンスバウンダリの下でグループ化されているため、これらのコンポーネントは常に同時に「表示スタート」となります。

import { Suspense } from 'react';
import Albums from './Albums.js';
import Biography from './Biography.js';
import Panel from './Panel.js';

export default function ArtistPage({ artist }) {
  return (
    <>
      <h1>{artist.name}</h1>
      <Suspense fallback={<Loading />}>
        <Biography artistId={artist.id} />
        <Panel>
          <Albums artistId={artist.id} />
        </Panel>
      </Suspense>
    </>
  );
}

function Loading() {
  return <h2>🌀 Loading...</h2>;
}

データをロードするコンポーネントは、サスペンスバウンダリの直接の子である必要はありません。例えば、BiographyAlbums を新しい Details コンポーネント内に移動することができます。これによって振る舞いは変わりません。BiographyAlbums の最も近い親のサスペンスバウンダリは同じですので、その表示開始は同時になるよう調整されます。

<Suspense fallback={<Loading />}>
<Details artistId={artist.id} />
</Suspense>

function Details({ artistId }) {
return (
<>
<Biography artistId={artistId} />
<Panel>
<Albums artistId={artistId} />
</Panel>
</>
);
}

ネストされたコンテンツをロード順に表示する

コンポーネントがサスペンドすると、最も近い親のサスペンスコンポーネントがフォールバックを表示します。この仕組みを使うと、複数のサスペンスコンポーネントをネストして、段階的なロードを構築することができます。各サスペンスバウンダリのフォールバックは、1 レベル下にあるコンテンツが利用可能になると実コンテンツで置き換わります。例えば、アルバムリストだけに独自のフォールバックを設定することができます。

<Suspense fallback={<BigSpinner />}>
<Biography />
<Suspense fallback={<AlbumsGlimmer />}>
<Panel>
<Albums />
</Panel>
</Suspense>
</Suspense>

これにより、Biography の表示が Albums のロードを「待つ」必要はなくなります。

以下のような順番になります。

  1. Biography がまだロードされていない場合、BigSpinner が全体のコンテンツエリアの代わりに表示されます。
  2. Biography のロードが完了すると、BigSpinner はコンテンツに置き換えられます。
  3. Albums がまだロードされていない場合、AlbumsGlimmerAlbums とその親の Panel の代わりに表示されます。
  4. 最後に、Albums のロードが完了すると、それが AlbumsGlimmer を置き換えて表示されます。
import { Suspense } from 'react';
import Albums from './Albums.js';
import Biography from './Biography.js';
import Panel from './Panel.js';

export default function ArtistPage({ artist }) {
  return (
    <>
      <h1>{artist.name}</h1>
      <Suspense fallback={<BigSpinner />}>
        <Biography artistId={artist.id} />
        <Suspense fallback={<AlbumsGlimmer />}>
          <Panel>
            <Albums artistId={artist.id} />
          </Panel>
        </Suspense>
      </Suspense>
    </>
  );
}

function BigSpinner() {
  return <h2>🌀 Loading...</h2>;
}

function AlbumsGlimmer() {
  return (
    <div className="glimmer-panel">
      <div className="glimmer-line" />
      <div className="glimmer-line" />
      <div className="glimmer-line" />
    </div>
  );
}

サスペンスバウンダリを使用することで、UI のどの部分は同時に「表示スタート」すべきで、どの部分はロード状態の進行につれて徐々にコンテンツを表示すべきなのか、調整を行えます。ツリー内のどこでサスペンスバウンダリの追加、移動、あるいは削除を行っても、アプリの他の動作に影響を与えることはありません。

あらゆるコンポーネントの周りにサスペンスバウンダリを置こうとしないようにしてください。ユーザに見せたいロードの各ステップよりもサスペンスバウンダリを細かく設置すべきではありません。デザイナと一緒に作業している場合は、ロード中表示をどこに配置するべきか尋ねてみてください。おそらく、それはデザインの枠組みにすでに含まれているでしょう。


新しいコンテンツのロード中に古いコンテンツを表示する

この例では、SearchResults コンポーネントは検索結果をフェッチする間サスペンドします。"a" を入力し、結果を待ってから "ab" に書き換えてみてください。"a" の検索結果が、ロード中フォールバックに置換されてしまいます。

import { Suspense, useState } from 'react';
import SearchResults from './SearchResults.js';

export default function App() {
  const [query, setQuery] = useState('');
  return (
    <>
      <label>
        Search albums:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Loading...</h2>}>
        <SearchResults query={query} />
      </Suspense>
    </>
  );
}

この代わりに一般的に使われる UI パターンは、結果リストの更新を遅延させて、新しい結果が準備できるまで前の結果を表示し続けるというものです。useDeferredValue フックを使うことで遅延されたバージョンのクエリ文字列を下に渡すことができます。

export default function App() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
<>
<label>
Search albums:
<input value={query} onChange={e => setQuery(e.target.value)} />
</label>
<Suspense fallback={<h2>Loading...</h2>}>
<SearchResults query={deferredQuery} />
</Suspense>
</>
);
}

query の方はすぐに更新されるため、入力フィールドは新しい値を表示します。しかし、deferredQuery はデータが読み込まれるまで前の値を保持するため、SearchResults はしばらく古い結果を表示します。

ユーザにより明確に状況を伝えるため、古い結果リストが表示されているときにインジケータを表示することができます。

<div style={{
opacity: query !== deferredQuery ? 0.5 : 1
}}>
<SearchResults query={deferredQuery} />
</div>

以下の例で "a" を入力し、結果がロードされるのを待ち、次に入力フィールドを "ab" に編集してみてください。新しい結果がロードされるまで、サスペンスのフォールバックの代わりに、暗くなった古い結果リストが表示されることに気づくでしょう。

import { Suspense, useState, useDeferredValue } from 'react';
import SearchResults from './SearchResults.js';

export default function App() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;
  return (
    <>
      <label>
        Search albums:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Loading...</h2>}>
        <div style={{ opacity: isStale ? 0.5 : 1 }}>
          <SearchResults query={deferredQuery} />
        </div>
      </Suspense>
    </>
  );
}

補足

値の遅延 (deferred value) とトランジションはいずれも、サスペンスフォールバックの表示を防いで代わりにインラインでインジケータを表示するために使えます。トランジションは更新の全体を低緊急度 (non-urgent) であるとマークするため、通常はフレームワークやルータライブラリでナビゲーションに使用されます。一方、値の遅延は主にアプリケーションコードで有用であり、UI の一部分を低緊急度とマークして、UI の他の部分に「遅れて」表示できるようにします。


すでに表示されているコンテンツが隠れるのを防ぐ

コンポーネントがサスペンドすると、直近の親のサスペンスバウンダリがフォールバック表示に切り替わります。すでに何らかのコンテンツを表示していた場合、これによりユーザ体験が不快になる可能性があります。このボタンを押してみてください。

import { Suspense, useState } from 'react';
import IndexPage from './IndexPage.js';
import ArtistPage from './ArtistPage.js';
import Layout from './Layout.js';

export default function App() {
  return (
    <Suspense fallback={<BigSpinner />}>
      <Router />
    </Suspense>
  );
}

function Router() {
  const [page, setPage] = useState('/');

  function navigate(url) {
    setPage(url);
  }

  let content;
  if (page === '/') {
    content = (
      <IndexPage navigate={navigate} />
    );
  } else if (page === '/the-beatles') {
    content = (
      <ArtistPage
        artist={{
          id: 'the-beatles',
          name: 'The Beatles',
        }}
      />
    );
  }
  return (
    <Layout>
      {content}
    </Layout>
  );
}

function BigSpinner() {
  return <h2>🌀 Loading...</h2>;
}

ボタンを押した時点で、Router コンポーネントは IndexPage の代わりに ArtistPage をレンダーしました。ArtistPage 内のコンポーネントがサスペンドしたため、最も近いサスペンスバウンダリがフォールバックを表示し始めました。最も近いサスペンスバウンダリはルート近くにあったため、サイト全体のレイアウトが BigSpinner に置き換えられてしまいました。

これを防ぐため、ナビゲーションの state 更新を startTransitionトランジションとしてマークすることができます。

function Router() {
const [page, setPage] = useState('/');

function navigate(url) {
startTransition(() => {
setPage(url);
});
}
// ...

これにより、React に対して state の遷移が緊急のものではなく、既に表示されている内容を隠すよりも前のページを表示し続ける方が良いと伝えます。これで、ボタンをクリックすると Biography の読み込みを「待つ」ようになります。

import { Suspense, startTransition, useState } from 'react';
import IndexPage from './IndexPage.js';
import ArtistPage from './ArtistPage.js';
import Layout from './Layout.js';

export default function App() {
  return (
    <Suspense fallback={<BigSpinner />}>
      <Router />
    </Suspense>
  );
}

function Router() {
  const [page, setPage] = useState('/');

  function navigate(url) {
    startTransition(() => {
      setPage(url);
    });
  }

  let content;
  if (page === '/') {
    content = (
      <IndexPage navigate={navigate} />
    );
  } else if (page === '/the-beatles') {
    content = (
      <ArtistPage
        artist={{
          id: 'the-beatles',
          name: 'The Beatles',
        }}
      />
    );
  }
  return (
    <Layout>
      {content}
    </Layout>
  );
}

function BigSpinner() {
  return <h2>🌀 Loading...</h2>;
}

トランジションはすべてのコンテンツの読み込みを待機するわけではありません。既に表示されたコンテンツを隠さない範囲でのみ待機を行います。例えば、ウェブサイトの Layout は既に表示されていたので、それをローディングスピナで隠すのは良くありません。しかし、その内部で Albums を囲んでいる Suspense バウンダリは新しいものなので、トランジションがそれを待つことはありません。

補足

サスペンス対応のルータは、デフォルトでナビゲーションの更新をトランジションにラップすることが期待されます。


トランジションが進行中であることを示す

上記の例では、ボタンをクリックした後、ナビゲーションが進行中であることを視覚的に示すものがありません。インジケータを追加するために、startTransitionuseTransition に置き換えることで、ブーリアン型の isPending 値が得られます。以下の例では、トランジションが進行中である間、ウェブサイトのヘッダのスタイルを変更するために使用されています。

import { Suspense, useState, useTransition } from 'react';
import IndexPage from './IndexPage.js';
import ArtistPage from './ArtistPage.js';
import Layout from './Layout.js';

export default function App() {
  return (
    <Suspense fallback={<BigSpinner />}>
      <Router />
    </Suspense>
  );
}

function Router() {
  const [page, setPage] = useState('/');
  const [isPending, startTransition] = useTransition();

  function navigate(url) {
    startTransition(() => {
      setPage(url);
    });
  }

  let content;
  if (page === '/') {
    content = (
      <IndexPage navigate={navigate} />
    );
  } else if (page === '/the-beatles') {
    content = (
      <ArtistPage
        artist={{
          id: 'the-beatles',
          name: 'The Beatles',
        }}
      />
    );
  }
  return (
    <Layout isPending={isPending}>
      {content}
    </Layout>
  );
}

function BigSpinner() {
  return <h2>🌀 Loading...</h2>;
}


ナビゲーション時にサスペンスバウンダリをリセットする

トランジション中、React は既に表示されているコンテンツを隠さないようにします。しかし、異なるパラメータを持つルートに移動する場合、React にそれが異なるコンテンツであると伝えたいことがあります。これを表現するために、key が使えます。

<ProfilePage key={queryParams.id} />

単一のユーザのプロフィールページ内を閲覧していて、何かがサスペンドすると想像してみてください。その更新がトランジションでラップされている場合、既に表示されているコンテンツのフォールバックをトリガしません。これは期待される動作です。

しかし、2 人の異なるユーザのプロフィール間を移動していると想像してみてください。その場合は、フォールバックを表示することが理にかなっています。例えば、あるユーザのタイムラインは別のユーザのタイムラインとは異なるコンテンツです。key を指定することで、React は異なるユーザのプロフィールを異なるコンポーネントとして扱うので、ナビゲーション中にサスペンスバウンダリをリセットします。サスペンスを統合したルータは、これを自動的に行うべきです。


サーバエラー用およびクライアント専用コンテンツ用のフォールバックを指定する

ストリーミングサーバレンダリング API のいずれか(またはそれらに依存するフレームワーク)を使用する場合も、React は <Suspense> バウンダリを使用してサーバ上のエラーを処理します。コンポーネントがサーバ上でエラーをスローしても、React はサーバレンダリングを中止しません。代わりに、上位の最も近い <Suspense> コンポーネントを見つけ、そのフォールバック(スピナなど)を、生成されたサーバ HTML に含めます。ユーザには最初にスピナが見えることになります。

クライアント側では、React は同じコンポーネントを再度レンダーしようとします。クライアントでもエラーが発生すると、React はエラーをスローし、最も近いエラーバウンダリを表示します。しかし、クライアントでエラーが発生しない場合は、最終的にコンテンツが正常に表示されたということになるため、React はユーザにエラーを表示しません。

これを使用して、サーバ上で一部のコンポーネントのレンダーを明示的に拒否することができます。これを行うには、サーバ環境ではエラーをスローするようにし、コンポーネントを <Suspense> バウンダリにラップして、HTML の代わりにフォールバックが表示されるようにします。

<Suspense fallback={<Loading />}>
<Chat />
</Suspense>

function Chat() {
if (typeof window === 'undefined') {
throw Error('Chat should only render on the client.');
}
// ...
}

サーバからの HTML にはローディングインジケータが含まれます。クライアント上で Chat コンポーネントに置き換わります。


トラブルシューティング

更新中に UI がフォールバックに置き換わるのを防ぐ方法は?

すでに表示中の UI をフォールバックに置き換えると、ユーザ体験が不快になります。これは、更新がコンポーネントをサスペンドさせるが、最も近いサスペンスバウンダリがすでにユーザにコンテンツを表示している、という場合に発生します。

これを防ぐには、startTransition を使用して更新を低緊急度としてマークします。トランジション中、React は十分なデータがロードされるまで待機し、不要なフォールバックが表示されるのを防ぎます。

function handleNextPageClick() {
// If this update suspends, don't hide the already displayed content
startTransition(() => {
setCurrentPage(currentPage + 1);
});
}

これにより、既存のコンテンツが隠されるのを避けることができます。ただし、新たにレンダーされる Suspense バウンダリは、UI をブロックするのを避け、利用可能になったらユーザがコンテンツを見ることができるよう、すぐにフォールバックを表示します。

React が不要なフォールバックを防ぐのは、低緊急度の更新中のみです。緊急の更新の結果としてレンダーが発生した場合、レンダーの遅延は起こりません。startTransitionuseDeferredValue のような API を使用して明示的にオプトインする必要があります。

あなたのルータがサスペンスと統合されている場合、更新は自動的に startTransition でラップされているはずです。