<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 内のツリー全体が単一のユニットとして扱われます。たとえば、これらのコンポーネントの1 つだけが何らかのデータを待って中断した場合でも、それらすべてが一緒に読み込みインジケーターに置き換えられます。
<Suspense fallback={<Loading />}>
<Biography />
<Panel>
<Albums />
</Panel>
</Suspense>
その後、それらすべてが表示される準備が整うと、すべてが同時に一緒に表示されます。
以下の例では、Biography
と Albums
の両方がいくつかのデータをフェッチします。ただし、それらは単一の 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 バウンダリーの直接の子である必要はありません。たとえば、Biography
と Albums
を新しい Details
コンポーネントに移動できます。これにより、動作は変更されません。Biography
と Albums
は同じ最も近い親 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
の読み込みを「待つ」必要がなくなります。
シーケンスは次のようになります
Biography
がまだ読み込まれていない場合は、コンテンツ領域全体にBigSpinner
が表示されます。Biography
の読み込みが完了すると、BigSpinner
はコンテンツに置き換えられます。Albums
がまだ読み込まれていない場合は、Albums
とその親のPanel
の代わりにAlbumsGlimmer
が表示されます。- 最後に、
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境界がフォールバックの表示に切り替わります。すでに何らかのコンテンツが表示されていた場合、これはユーザーエクスペリエンスを損なう可能性があります。このボタンを押してみてください。
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
境界は新しいものなので、トランジションはそれを待機しません。
トランジションが発生していることを示す
上記の例では、ボタンをクリックしても、ナビゲーションが進行中であることを示す視覚的な表示はありません。インジケーターを追加するには、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
で自動的にラップする必要があります。