<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
,除非導致它的更新是由startTransition
或useDeferredValue
引起的。 - 如果 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 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
尚未載入,則會顯示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> ); }
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> </> ); }
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
而不是 IndexPage
。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>; }
轉場不會等待*所有*內容載入。它只會等待足夠長的時間,以避免隱藏已顯示的內容。例如,網站 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>; }
在導覽時重置 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 只會在非緊急更新期間防止不必要的後備內容。如果是緊急更新的結果,它不會延遲渲染。您必須使用 startTransition
或 useDeferredValue
等 API 選擇加入。
如果您的路由器與 Suspense 整合,它應該會自動將其更新包裝到 startTransition
中。