<Suspense> は、子要素の読み込みが完了するまでフォールバックを表示できます。

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

リファレンス

<Suspense>

Props

  • children: レンダリングする実際のUI。 children がレンダリング中に中断した場合、Suspense境界は fallback のレンダリングに切り替わります。
  • fallback: 実際のUIが読み込みを完了していない場合に、代わりにレンダリングする代替UI。 有効なReactノードであれば何でも受け入れられますが、実際にはフォールバックは、ローディングスピナーやスケルトンなど、軽量なプレースホルダービューです。 Suspenseは、children が中断したときに自動的に fallback に切り替え、データが準備できると children に戻ります。 fallback がレンダリング中に中断すると、最も近い親Suspense境界がアクティブになります。

注意点

  • Reactは、最初にマウントされる前に中断したレンダリングの状態を保持しません。コンポーネントがロードされると、Reactは中断されたツリーのレンダリングを最初から再試行します。
  • Suspenseがツリーのコンテンツを表示していたが、その後再び中断した場合、fallback は、それを引き起こした更新が startTransition または useDeferredValue によって引き起こされたものでない限り、再び表示されます。
  • Reactが再び中断したためにすでに表示されているコンテンツを非表示にする必要がある場合、コンテンツツリー内の レイアウトエフェクト をクリーンアップします。コンテンツを再び表示する準備が整うと、Reactはレイアウトエフェクトを再度実行します。これにより、DOMレイアウトを測定するエフェクトが、コンテンツが非表示になっている間にこれを実行しようとしないようにします。
  • Reactには、Suspenseと統合された *ストリーミングサーバーレンダリング* や *セレクティブハイドレーション* のような内部最適化が含まれています。詳細については、アーキテクチャの概要を読み、技術講演をご覧ください。

使用法

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

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

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

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

以下の例では、Albums コンポーネントはアルバムのリストをフェッチしている間、中断します。レンダリングの準備ができるまで、React は上にある最も近い Suspense バウンダリーを切り替えて、フォールバックである 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>;
}

注意

Suspense に対応したデータソースのみが Suspense コンポーネントをアクティブにします。それらには以下が含まれます

  • RelayNext.js などの Suspense に対応したフレームワークによるデータのフェッチ
  • lazy を使用したコンポーネントコードの遅延読み込み
  • use を使用したキャッシュされた Promise の値の読み取り

Suspense は、Effect またはイベントハンドラー内でデータがフェッチされたときは検出しません

上記の Albums コンポーネントでデータを読み込む正確な方法は、フレームワークによって異なります。Suspense に対応したフレームワークを使用する場合は、そのデータフェッチのドキュメントに詳細が記載されています。

意見のあるフレームワークを使用しない Suspense 対応のデータフェッチは、まだサポートされていません。Suspense 対応のデータソースを実装するための要件は、不安定でドキュメント化されていません。データソースを Suspense と統合するための公式 API は、React の将来のバージョンでリリースされる予定です。


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

デフォルトでは、Suspense 内のツリー全体が単一のユニットとして扱われます。たとえば、これらのコンポーネントの1 つだけが何らかのデータを待って中断した場合でも、それらすべてが一緒に読み込みインジケーターに置き換えられます。

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

その後、それらすべてが表示される準備が整うと、すべてが同時に一緒に表示されます。

以下の例では、BiographyAlbums の両方がいくつかのデータをフェッチします。ただし、それらは単一の Suspense バウンダリーの下にグループ化されているため、これらのコンポーネントは常に同時に一緒に「ポップイン」します。

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>;
}

データを読み込むコンポーネントは、Suspense バウンダリーの直接の子である必要はありません。たとえば、BiographyAlbums を新しい Details コンポーネントに移動できます。これにより、動作は変更されません。BiographyAlbums は同じ最も近い親 Suspense バウンダリーを共有するため、それらの表示は一緒に調整されます。

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

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

読み込み時にネストされたコンテンツを表示する

コンポーネントが中断すると、最も近い親の Suspense コンポーネントがフォールバックを表示します。これにより、複数の Suspense コンポーネントをネストして読み込みシーケンスを作成できます。各 Suspense バウンダリーのフォールバックは、次のレベルのコンテンツが利用可能になるにつれて埋められます。たとえば、アルバムリストに独自のフォールバックを設定できます

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

この変更により、Biography の表示は、Albums の読み込みを「待つ」必要がなくなります。

シーケンスは次のようになります

  1. Biography がまだ読み込まれていない場合は、コンテンツ領域全体に BigSpinner が表示されます。
  2. Biography の読み込みが完了すると、BigSpinner はコンテンツに置き換えられます。
  3. Albums がまだ読み込まれていない場合は、Albums とその親の Panel の代わりに AlbumsGlimmer が表示されます。
  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>
  );
}

Suspense バウンダリーを使用すると、UI のどの部分を常に同時に「ポップイン」させるか、およびどの部分を読み込み状態のシーケンスで段階的に表示するかを調整できます。アプリの残りの動作に影響を与えることなく、ツリー内の任意の場所に Suspense バウンダリーを追加、移動、削除できます。

すべてのコンポーネントの周りに Suspense バウンダリーを配置しないでください。Suspense バウンダリーは、ユーザーに体験させたい読み込みシーケンスよりも細かくしないでください。デザイナーと一緒に作業する場合は、読み込み状態をどこに配置する必要があるかを確認してください。それらはデザインのワイヤーフレームにすでに含まれている可能性が高くなります。


新しいコンテンツの読み込み中に古いコンテンツを表示する

この例では、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」に編集してください。Suspenseフォールバックの代わりに、新しい結果がロードされるまで、薄暗い古い結果リストが表示されることに注意してください。

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>
    </>
  );
}

注意

遅延値とトランジションの両方を使用すると、インラインインジケータの代わりにSuspenseフォールバックを表示することを回避できます。トランジションは、更新全体を緊急ではないものとしてマークするため、通常、フレームワークやルーターライブラリでナビゲーションに使用されます。一方、遅延値は、UIの一部を緊急ではないものとしてマークし、UIの残りの部分から「遅れる」ようにしたいアプリケーションコードで最も役立ちます。


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

コンポーネントがサスペンドすると、最も近い親のSuspense境界がフォールバックの表示に切り替わります。すでに何らかのコンテンツが表示されていた場合、これはユーザーエクスペリエンスを損なう可能性があります。このボタンを押してみてください。

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内のコンポーネントがサスペンドしたため、最も近いSuspense境界がフォールバックの表示を開始しました。最も近いSuspense境界はルートの近くにあったため、サイトレイアウト全体がBigSpinnerに置き換えられました。

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

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

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

これにより、Reactは状態遷移が緊急ではなく、すでに表示されているコンテンツを非表示にするよりも前のページを表示し続ける方がよいことを認識します。これで、ボタンをクリックすると、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>;
}

トランジションは、すべてのコンテンツがロードされるのを待つわけではありません。すでに表示されているコンテンツを非表示にすることを避けるために十分な時間だけ待機します。たとえば、WebサイトのLayoutはすでに表示されているため、ローディングスピナーの背後に隠してしまうのはよくありません。ただし、Albumsの周りのネストされたSuspense境界は新しいものなので、トランジションはそれを待機しません。

注意

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


トランジションが発生していることを示す

上記の例では、ボタンをクリックしても、ナビゲーションが進行中であることを示す視覚的な表示はありません。インジケーターを追加するには、startTransitionを、ブール値のisPending値を提供するuseTransitionに置き換えることができます。下の例では、トランジションの発生中にWebサイトのヘッダースタイルを変更するために使用されています。

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>;
}


ナビゲーション時にSuspense境界をリセットする

トランジション中、Reactはすでに表示されているコンテンツを非表示にすることを避けます。ただし、異なるパラメーターを持つルートに移動する場合は、それが異なるコンテンツであることをReactに伝える必要があります。keyを使用して、これを表現できます。

<ProfilePage key={queryParams.id} />

ユーザーのプロファイルページ内でナビゲートしていて、何かがサスペンドするとします。その更新がトランジションでラップされている場合、すでに表示されているコンテンツのフォールバックはトリガーされません。これは予期された動作です。

ただし、2つの異なるユーザープロファイル間をナビゲートしているとします。その場合、フォールバックを表示するのが理にかなっています。たとえば、あるユーザーのタイムラインは、別のユーザーのタイムラインとは異なるコンテンツです。keyを指定することで、Reactが異なるユーザーのプロファイルを異なるコンポーネントとして扱い、ナビゲーション中にSuspense境界をリセットするようにします。Suspenseが統合されたルーターは、これを自動的に行う必要があります。


サーバーエラーとクライアント専用コンテンツのフォールバックの提供

ストリーミングサーバーレンダリング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をフォールバックに置き換えると、ユーザーエクスペリエンスがぎくしゃくしたものになります。これは、更新によってコンポーネントが中断し、最も近いSuspense境界が既にユーザーにコンテンツを表示している場合に発生する可能性があります。

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

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

これにより、既存のコンテンツの非表示が回避されます。ただし、新しくレンダリングされたSuspense境界は、UIをブロックすることを避けてコンテンツが利用可能になるとすぐにユーザーに見えるように、引き続きすぐにフォールバックを表示します。

Reactは、緊急ではない更新中にのみ、不要なフォールバックを防ぎます。緊急の更新の結果である場合、レンダリングを遅延させません。startTransitionまたはuseDeferredValueのようなAPIを使用してオプトインする必要があります。

ルーターがSuspenseと統合されている場合、更新をstartTransitionで自動的にラップする必要があります。