<Suspense>
<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>; }
コンテンツを一度にまとめて表示する
デフォルトでは、サスペンス内の全てのツリーはひとつの単位として扱われます。例えば、以下のコンポーネントのうちどれかひとつでもデータ待ちでサスペンドしていれば、すべてがまとめてローディングインジケータに置き換わります。
<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
のロードを「待つ」必要はなくなります。
以下のような順番になります。
Biography
がまだロードされていない場合、BigSpinner
が全体のコンテンツエリアの代わりに表示されます。Biography
のロードが完了すると、BigSpinner
はコンテンツに置き換えられます。Albums
がまだロードされていない場合、AlbumsGlimmer
がAlbums
とその親のPanel
の代わりに表示されます。- 最後に、
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> </> ); }
すでに表示されているコンテンツが隠れるのを防ぐ
コンポーネントがサスペンドすると、直近の親のサスペンスバウンダリがフォールバック表示に切り替わります。すでに何らかのコンテンツを表示していた場合、これによりユーザ体験が不快になる可能性があります。このボタンを押してみてください。
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>; }
ナビゲーション時にサスペンスバウンダリをリセットする
トランジション中、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 が不要なフォールバックを防ぐのは、低緊急度の更新中のみです。緊急の更新の結果としてレンダーが発生した場合、レンダーの遅延は起こりません。startTransition
や useDeferredValue
のような API を使用して明示的にオプトインする必要があります。
あなたのルータがサスペンスと統合されている場合、更新は自動的に startTransition
でラップされているはずです。