<Suspense>

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

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

リファレンス

<Suspense>

props

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

注意点

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

サスペンスバウンダリがアクティブになる条件

サスペンスバウンダリは、コンテンツの準備ができるまでその表示を待機します。以下のいずれかに該当する間、バウンダリはコンテンツを表示しません。

  • lazy によってコンポーネントコードを遅延ロードしている。
  • use でプロミスを読み取っている。サーバコンポーネントからストリーミングされたデータや、サスペンス対応フレームワークを介して読み込まれたデータも含む。
  • <link rel="stylesheet"> と precedence プロパティを使ってレンダーされたスタイルシートを読み込んでいる。React は、タイムアウト時間を上限として、スタイルシートが読み込まれるのをバウンダリで待機します。以下の例を参照。
  • 大きなバウンダリ内で、ストリーミングサーバレンダリング経由で HTML が到着するのを待機している。HTML の送信には時間がかかるため、一定以上の量のコンテンツを持つバウンダリは、その内部で何もサスペンドしていなくてもアクティブになります。React は HTML の到着に合わせてコンテンツを表示します。
  • Canary only フォントを読み込んでいる。デフォルトではサスペンスはフォントを待機しませんが、<ViewTransition> による更新では、テキストがフォールバックフォントで一瞬表示されないよう、タイムアウト時間を上限として新しいフォントの読み込みを待機します。以下の例を参照。
  • Canary only 画像を読み込んでいる。デフォルトではサスペンスは画像を待ちませんが、<ViewTransition> による更新中は、タイムアウト時間を上限として React がバウンダリで画像の読み込みを待機します。onLoad ハンドラを追加すると、個別の画像をこの動作の対象外にできます。以下の例を参照。
  • Experimental only <Suspense defer> バウンダリ内で CPU 負荷の高いレンダー処理を実行している。

補足

サスペンス対応フレームワーク

サスペンス対応フレームワークを使うと、最も近いサスペンスバウンダリをアクティブにする形で、コンポーネントからデータを読み取れます。実際にデータを読み込む方法はフレームワークによって異なるため、詳細は各フレームワークのドキュメントを参照してください。内部では、サスペンス対応フレームワークがプロミスのキャッシュを管理し、use を呼び出してプロミスに対してサスペンドします。

フレームワークを使用しない場合でも、同じインスタンスが複数回のレンダーで再利用されるようにプロミスがキャッシュされていれば、use でプロミスを直接読み取れます。


使用法

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

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

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

対照的に、エフェクト内など、use の外でデータをフェッチするコードによってバウンダリがアクティブになることはありません。

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

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

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

ストリーミングサーバレンダリング中は、HTML のストリーミング中もバウンダリがアクティブになります。どのストリーミングサーバレンダリング API でも、React は最初に fallback を含んだシェルを送信し、続いて各バウンダリの HTML をストリーミングして、コンテンツが到着するたびに fallback と入れ替えます。“Render the page” を押して、ページがストリーミングされる様子を確認してください。

import { flushReadableStreamToFrame } from './demo-helpers.js';
import { Suspense, use } from 'react';
import { renderToReadableStream } from 'react-dom/server';

let posts = null;

function Posts() {
  const text = use(posts.promise);
  return <p>{text}</p>;
}

function ProfilePage() {
  return (
    <html>
      <body>
        <h1>Alice</h1>
        <p>Photographer and traveler.</p>
        <Suspense fallback={<p>⌛ Loading posts...</p>}>
          <Posts />
        </Suspense>
      </body>
    </html>
  );
}

async function main(frame) {
  posts = Promise.withResolvers();
  const stream = await renderToReadableStream(<ProfilePage />);

  // The posts resolve after the shell has streamed, so React
  // streams their HTML in and swaps out the fallback.
  setTimeout(() => {
    posts.resolve(
      'Just got back from two weeks along the coast. The drive ' +
      'was longer than expected, but every stop was worth it. ' +
      'A full write-up and more photos are coming soon.'
    );
  }, 1500);

  await flushReadableStreamToFrame(stream, frame);
}

document.getElementById('render').addEventListener('click', () => {
  main(document.getElementById('container'));
});


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

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

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

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

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

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

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

<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 がまだロードされていない場合、AlbumsGlimmer が Albums とその親の 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 バウンダリは新しいものなので、トランジションがそれを待つことはありません。

補足

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


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

上記の例では、ボタンをクリックした後、ナビゲーションが進行中であることを視覚的に示すものがありません。インジケータを追加するために、startTransition を useTransition に置き換えることで、ブーリアン型の 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>;
}


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

トランジション (Transition) 中、React はすでに表示されているコンテンツを隠さないようにします。しかし、別ユーザのプロフィールなど、別種のコンテンツに切り替える場合は、既存のコンテンツではなくフォールバックをバウンダリに表示したいはずです。これは key で表現できます。

<ProfilePage key={queryParams.id} />

異なる key を指定すると、React はそれぞれのプロフィールを異なるコンテンツとして扱い、ナビゲーション中にサスペンスバウンダリをリセットします。key はバウンダリ自体にも、その上にあるコンポーネントにも指定できます。サスペンスと統合されたルータは、この処理を自動的に行う必要があります。

以下の例では、プロフィールページを開くと最初のプロフィールが読み込まれます。“Bob” を押すと別のプロフィールに移動し、key によってバウンダリがリセットされるため、既存ユーザの自己紹介ではなくフォールバックが表示されます。key を削除してみてください。次のプロフィールを読み込んでいる間も、既存の自己紹介が表示されたままになってしまいます。

import { Suspense, useState, startTransition } from 'react';
import Bio from './Bio.js';
import { fetchBio } from './data.js';

export default function ProfilePage() {
  const [user, setUser] = useState(() => ({
    id: 'alice',
    bioPromise: fetchBio('alice'),
  }));
  function navigate(id) {
    startTransition(() => {
      setUser({ id, bioPromise: fetchBio(id) });
    });
  }
  return (
    <>
      <button onClick={() => navigate('alice')}>
        Alice
      </button>
      <button onClick={() => navigate('bob')}>
        Bob
      </button>
      <Suspense key={user.id} fallback={<p>⌛ Loading profile...</p>}>
        <Bio bioPromise={user.bioPromise} />
      </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 コンポーネントに置き換わります。


Canary only ブラウザ専用コンテンツにフォールバックを提供する

サスペンスバウンダリを使って、ブラウザ専用コンポーネントにフォールバックを提供できます。コンポーネントを <Suspense> でラップし、その内部で use(browser()) を呼び出します。

Reload をクリックすると、初期 HTML 内のローディングフォールバックを確認できます。ハイドレーション後、React は localStorage から読み込んだ下書きを表示します。

import { Suspense, use, useState } from 'react';
import { browser } from 'react-dom';

function SavedDraft() {
  use(browser('The draft is stored in localStorage.'));
  const [draft, setDraft] = useState(
    () => localStorage.getItem('draft') ?? ''
  );

  function handleChange(event) {
    const nextDraft = event.target.value;
    setDraft(nextDraft);
    localStorage.setItem('draft', nextDraft);
  }

  return (
    <label>
      Draft:
      <textarea
        value={draft}
        onChange={handleChange}
        rows={4}
        cols={30}
      />
    </label>
  );
}

export default function App() {
  return (
    <>
      <h1>Saved draft</h1>
      <Suspense fallback={<p>Loading draft...</p>}>
        <SavedDraft />
      </Suspense>
    </>
  );
}

サーバレンダリング中、React はサスペンスバウンダリのフォールバックを HTML に含めます。ブラウザでは、React がフォールバックを保存済みの下書きに置き換えます。


スタイルシートの読み込みを待機する

<link rel="stylesheet"> と precedence プロパティを使ってレンダーされたスタイルシートがある場合、コンテンツがスタイル未適用で表示されないよう、React はタイムアウト時間を上限として、そのスタイルシートが読み込まれるまでサスペンスバウンダリ内のコンテンツの表示を待機します。

以下の例では、Card コンポーネントが precedence を指定したスタイルシートをレンダーします。“Show card” を押してください。React はスタイルシートが読み込まれるまでフォールバックを表示し、その後、スタイルが適用されたカードを表示します。

比較のため、2 番目のボタンは別のドキュメント内で React を使わずに同じ更新を行います。スタイルシートの読み込みを待機するものがないため、カードのテキストは最初にフォールバックフォントで表示され、その後切り替わります。

import { Suspense, useState, startTransition } from 'react';
import { freshStylesheetUrl } from './styles.js';
import VanillaCard from './VanillaCard.js';

function Card({ href }) {
  return (
    <>
      <link rel="stylesheet" href={href} precedence="default" />
      <div className="fancy-card">This card uses a font from the stylesheet.</div>
    </>
  );
}

export default function App() {
  const [href, setHref] = useState(null);
  return (
    <>
      <button
        onClick={() => {
          startTransition(() => {
            setHref(freshStylesheetUrl());
          });
        }}>
        Show card
      </button>
      {href && (
        <Suspense fallback={<p>⌛ Loading styles...</p>}>
          <Card href={href} />
        </Suspense>
      )}
      <hr />
      <VanillaCard />
    </>
  );
}


Canary only サスペンスのコンテンツからアニメーションする

サスペンスと <ViewTransition> を組み合わせて、フォールバックからコンテンツへの入れ替えをアニメーションできます。バウンダリを <ViewTransition> でラップすると、React は入れ替えを更新として扱い、デフォルトではフォールバックとコンテンツをクロスフェードさせます。

import {ViewTransition, useState, startTransition, Suspense} from 'react';
import {Video, VideoPlaceholder} from './Video';
import {useLazyVideoData} from './data';

function LazyVideo() {
  const video = useLazyVideoData();
  return <Video video={video} />;
}

export default function Component() {
  const [showItem, setShowItem] = useState(false);
  return (
    <>
      <button
        onClick={() => {
          startTransition(() => {
            setShowItem((prev) => !prev);
          });
        }}>
        {showItem ? '➖' : '➕'}
      </button>
      {showItem ? (
        <ViewTransition>
          <Suspense fallback={<VideoPlaceholder />}>
            <LazyVideo />
          </Suspense>
        </ViewTransition>
      ) : null}
    </>
  );
}

補足

バウンダリに対して <ViewTransition> を配置する相対位置によって、フォールバックとコンテンツが 1 回の更新としてクロスフェードするか、それぞれ exit と enter のアニメーションとして動作するかが決まります。ビュー遷移クラスを使ってアニメーションをカスタマイズすることもできます。

サスペンスのコンテンツからのアニメーションについて詳しく読む。


Canary only フォントの読み込みを待機する

<ViewTransition> がサスペンスバウンダリの内容表示をアニメーションする際、テキストがフォールバックフォントで一瞬表示されてしまわないよう、React はタイムアウト時間を上限として、コンテンツが導入する新しいフォントの読み込みを待機します。これは <ViewTransition> による更新中にのみ行われます。

以下の例では、サスペンスバウンダリが <ViewTransition> でラップされており、Quote コンポーネントはデータの読み込み中にサスペンドします。引用文をレンダーすると、フォントのダウンロードが始まります。React はフォントが読み込まれるまでフォールバックを表示し続けるため、引用文は最初からそのフォントで表示されます。

比較のため、2 番目のボタンは React を使わずに同じ更新を行います。フォントの読み込みを待機するものがないため、テキストは最初にフォールバックフォントで表示され、その後切り替わります。

import { ViewTransition, Suspense, use, useState, startTransition } from 'react';
import { fetchQuote } from './data.js';
import { freshFontUrl } from './font.js';
import VanillaQuote from './VanillaQuote.js';

function Quote({ fontSrc }) {
  const quote = use(fetchQuote());
  return (
    <>
      <style href={fontSrc} precedence="default">
        {`@font-face {
          font-family: 'Fancy';
          src: url(${fontSrc}) format('truetype');
          font-display: swap;
        }`}
      </style>
      <p className="quote fancy">{quote}</p>
    </>
  );
}

export default function App() {
  const [fontSrc, setFontSrc] = useState(null);
  return (
    <>
      <button
        onClick={() => {
          startTransition(() => {
            setFontSrc(freshFontUrl());
          });
        }}>
        Show quote
      </button>
      {fontSrc && (
        <ViewTransition>
          <Suspense fallback={<p className="quote">⌛ Loading quote...</p>}>
            <Quote fontSrc={fontSrc} />
          </Suspense>
        </ViewTransition>
      )}
      <hr />
      <VanillaQuote />
    </>
  );
}


Canary only 画像の読み込みを待機する

<ViewTransition> がサスペンスバウンダリの表示をアニメーションする際、読み込み途中の画像でアニメーションが始まってしまわないよう、React はタイムアウト時間を上限として、表示対象の画像が読み込まれるのを待機します。これは <ViewTransition> による更新中にのみ行われます。onLoad ハンドラを追加すると、<ViewTransition> 内であっても、個別の画像をこの動作の対象外にできます。

以下の例では、サスペンスバウンダリが <ViewTransition> でラップされており、ポートレート画像が読み込まれるまでプロフィールのスケルトンを表示します。

比較のため、2 番目のボタンは React を使わずに同じ更新を行います。画像の読み込みを待機するものがないため、カードがすぐに表示され、中の画像は読み込まれた時点で突然現れます。

import { ViewTransition, Suspense, useState, startTransition } from 'react';
import { freshImageUrl } from './image.js';
import VanillaProfile from './VanillaProfile.js';

function Profile({ src }) {
  return (
    <div className="card">
      <img src={src} alt="Jack Pope" width={80} height={80} />
      <p>Jack Pope</p>
    </div>
  );
}

function ProfilePlaceholder() {
  return (
    <div className="card">
      <div className="avatar-placeholder" />
      <p className="name-placeholder">&nbsp;</p>
    </div>
  );
}

export default function App() {
  const [src, setSrc] = useState(null);
  return (
    <>
      <button
        onClick={() => {
          startTransition(() => {
            setSrc(freshImageUrl());
          });
        }}>
        Show profile
      </button>
      {src && (
        <ViewTransition>
          <Suspense fallback={<ProfilePlaceholder />}>
            <Profile src={src} />
          </Suspense>
        </ViewTransition>
      )}
      <hr />
      <VanillaProfile />
    </>
  );
}


Canary only フォント、画像、スタイルシートを連携させる

サスペンスバウンダリは、データ、スタイルシート、フォント、画像をまとめて待機することができます。フォントと画像を待つのは、<ViewTransition> による更新中だけです。以下の例では、ProfileCard コンポーネントがデータの読み込み中にサスペンドし、precedence を指定したスタイルシート、新しいフォントのテキスト、ポートレート画像をレンダーします。React はデータとスタイルシートの読み込み中、スケルトンを表示し続けます。その後、<ViewTransition> による表示がフォントと画像を待機するため、カードは完成した状態で現れます。

比較のための React を使用しないバージョンでは、同じデータを読み込んでいますが、各リソースがそれぞれのタイミングで到着して表示されています。

import { ViewTransition, Suspense, use, useState, startTransition } from 'react';
import { fetchQuote } from './data.js';
import { freshStylesheetUrl, freshImageUrl } from './resources.js';
import VanillaProfileCard from './VanillaProfileCard.js';

function ProfileCard({ resources }) {
  const quote = use(resources.quotePromise);
  return (
    <>
      <link rel="stylesheet" href={resources.stylesheet} precedence="default" />
      <div className="profile-card">
        <img src={resources.image} alt="Jack Pope" width={80} height={80} />
        <div>
          <p className="name">Jack Pope</p>
          <p className="bio">{quote}</p>
        </div>
      </div>
    </>
  );
}

function ProfileCardPlaceholder() {
  return (
    <div className="profile-card">
      <div className="avatar-placeholder" />
      <div>
        <p className="name name-placeholder">&nbsp;</p>
        <p className="bio bio-placeholder">&nbsp;</p>
      </div>
    </div>
  );
}

export default function App() {
  const [resources, setResources] = useState(null);
  return (
    <>
      <button
        onClick={() => {
          startTransition(() => {
            setResources({
              quotePromise: fetchQuote(),
              stylesheet: freshStylesheetUrl(),
              image: freshImageUrl(),
            });
          });
        }}>
        Show profile
      </button>
      {resources && (
        <ViewTransition>
          <Suspense fallback={<ProfileCardPlaceholder />}>
            <ProfileCard resources={resources} />
          </Suspense>
        </ViewTransition>
      )}
      <hr />
      <VanillaProfileCard />
    </>
  );
}


トラブルシューティング

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

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

これを防ぐには、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 を使用して明示的にオプトインする必要があります。

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