<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 需要隐藏已经可见的内容,因为它再次暂停,它将清除内容树中的 布局 Effects。当内容准备好再次显示时,React 将再次触发布局 Effects。这确保了测量 DOM 布局的 Effects 不会在内容隐藏时尝试执行此操作。
- 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
钩子允许你传递一个延迟版本的查询
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
中。