<Suspense> 能讓您在子元件完成載入之前顯示一個預設畫面。

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

參考

<Suspense>

屬性

  • children:您要渲染的實際 UI。如果 children 在渲染時暫停,Suspense 邊界將切換到渲染 fallback
  • fallback:如果實際 UI 尚未完成載入,則要渲染的替代 UI。任何有效的 React 節點都被接受,但在實務中,預設畫面是一個輕量級的佔位符視圖,例如載入微調器或骨架。當 children 暫停時,Suspense 會自動切換到 fallback,并在資料準備就緒時切換回 children。如果 fallback 在渲染時暫停,它將啟動最接近的父 Suspense 邊界。

注意事項

  • 對於在第一次掛載之前就被暫停的渲染,React 不會保留任何狀態。當元件載入後,React 將從頭開始重試渲染暫停的樹狀結構。
  • 如果 Suspense 正在顯示樹狀結構的內容,但隨後再次暫停,則將再次顯示 fallback,除非導致它的更新是由 startTransitionuseDeferredValue 引起的。
  • 如果 React 需要隱藏已顯示的內容,因為它再次暫停,它將清除內容樹狀結構中的 配置效果 (Layout Effects)。當內容準備好再次顯示時,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 元件。 它們包括

  • 使用啟用 Suspense 的框架(例如 RelayNext.js)進行資料擷取
  • 使用 `lazy` 延遲載入元件程式碼
  • 使用 `use` 讀取快取 Promise 的值

Suspense 不會偵測在 Effect 或事件處理常式內擷取資料的情況。

在上面的 `Albums` 元件中載入資料的確切方式取決於您的框架。如果您使用啟用 Suspense 的框架,您可以在其資料擷取文件中找到詳細資訊。

尚未支援在不使用特定框架的情況下啟用 Suspense 的資料擷取。實作啟用 Suspense 的資料來源的要求不穩定且未記錄。在 React 的未來版本中將會發布用於將資料來源與 Suspense 整合的官方 API。


一次顯示所有內容

預設情況下,Suspense 內的整個樹狀結構會被視為一個單元。例如,即使只有_其中一個_元件暫停以等待某些資料,_所有_元件都會一起被載入指示器取代

<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` 載入。

序列將會是

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

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 Hook 讓您可以傳遞查詢的延遲版本

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 元件會渲染 ArtistPage 而不是 IndexPageArtistPage 內的元件暫停,因此最接近的 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>;
}

轉場不會等待*所有*內容載入。它只會等待足夠長的時間,以避免隱藏已顯示的內容。例如,網站 Layout 已經顯示,因此將其隱藏在載入指示器後面是不好的。但是,Albums 周圍的巢狀 Suspense 邊界是新的,因此轉場不會等待它。

注意事項

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


在導覽時重置 Suspense 邊界

在轉換期間,React 會避免隱藏已顯示的內容。但是,如果您導覽到具有不同參數的路由,您可能需要告訴 React 這是*不同的*內容。您可以使用 key 來表達這一點。

<ProfilePage key={queryParams.id} />

想像您正在使用者個人資料頁面中導覽,並且某些內容暫停。如果該更新包含在轉換中,則它不會觸發已顯示內容的後備內容。這是預期的行為。

但是,現在想像您正在兩個不同的使用者個人資料之間導覽。在這種情況下,顯示後備內容是有意義的。例如,一個使用者的時間軸與另一個使用者的時間軸是*不同的內容*。透過指定 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 只會在非緊急更新期間防止不必要的後備內容。如果是緊急更新的結果,它不會延遲渲染。您必須使用 startTransitionuseDeferredValue 等 API 選擇加入。

如果您的路由器與 Suspense 整合,它應該會自動將其更新包裝到 startTransition 中。