<Suspense>
允许你显示回退,直到其子级完成加载。
¥<Suspense>
lets you display a fallback until its children have finished loading.
<Suspense fallback={<Loading />}>
<SomeComponent />
</Suspense>
参考
¥Reference
<Suspense>
属性
¥Props
-
children
:你打算渲染的实际 UI。如果children
在渲染时暂停,则 Suspense 边界将切换到渲染fallback
。¥
children
: The actual UI you intend to render. Ifchildren
suspends while rendering, the Suspense boundary will switch to renderingfallback
. -
fallback
:如果尚未完成加载,则替代实际 UI 进行渲染。接受任何有效的 React 节点,但在实践中,回退是一个轻量级的占位符视图,例如加载加载控件或骨架。当children
挂起时 Suspense 会自动切换到fallback
,当数据准备好后又回到children
。如果fallback
在渲染时挂起,它将激活最近的父 Suspense 边界。¥
fallback
: An alternate UI to render in place of the actual UI if it has not finished loading. Any valid React node is accepted, though in practice, a fallback is a lightweight placeholder view, such as a loading spinner or skeleton. Suspense will automatically switch tofallback
whenchildren
suspends, and back tochildren
when the data is ready. Iffallback
suspends while rendering, it will activate the closest parent Suspense boundary.
注意事项
¥Caveats
-
React 不会为第一次挂载之前挂起的渲染保留任何状态。组件加载后,React 将重新尝试从头开始渲染挂起的树。
¥React does not preserve any state for renders that got suspended before they were able to mount for the first time. When the component has loaded, React will retry rendering the suspended tree from scratch.
-
如果 Suspense 正在显示树的内容,但随后再次暂停,则
fallback
将再次显示,除非导致它的更新是由startTransition
或useDeferredValue
引起的。¥If Suspense was displaying content for the tree, but then it suspended again, the
fallback
will be shown again unless the update causing it was caused bystartTransition
oruseDeferredValue
. -
如果 React 因为再次挂起需要隐藏已经可见的内容,它会清理内容树中的 布局副作用。当内容准备好再次显示时,React 将再次触发布局副作用。这确保了测量 DOM 布局的副作用不会在内容隐藏时尝试执行此操作。
¥If React needs to hide the already visible content because it suspended again, it will clean up layout Effects in the content tree. When the content is ready to be shown again, React will fire the layout Effects again. This ensures that Effects measuring the DOM layout don’t try to do this while the content is hidden.
-
React 包括与 Suspense 集成的底层优化,例如流式服务器渲染和选择性水化。阅读 架构概述 并监视 技术讲座 以了解更多信息。
¥React includes under-the-hood optimizations like Streaming Server Rendering and Selective Hydration that are integrated with Suspense. Read an architectural overview and watch a technical talk to learn more.
用法
¥Usage
在加载内容时显示回退
¥Displaying a fallback while content is loading
你可以使用 Suspense 边界封装应用的任何部分:
¥You can wrap any part of your application with a Suspense boundary:
<Suspense fallback={<Loading />}>
<Albums />
</Suspense>
React 将显示你的 加载回退 直到加载了 子级 所需的所有代码和数据。
¥React will display your loading fallback until all the code and data needed by the children has been loaded.
在下面的示例中,Albums
组件在获取专辑列表时挂起。在准备好渲染之前,React 会切换上方最近的 Suspense 边界以显示回退 - 你的 Loading
组件。然后,当数据加载时,React 隐藏 Loading
回退并使用数据渲染 Albums
组件。
¥In the example below, the Albums
component suspends while fetching the list of albums. Until it’s ready to render, React switches the closest Suspense boundary above to show the fallback—your Loading
component. Then, when the data loads, React hides the Loading
fallback and renders the Albums
component with data.
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>; }
一起显示内容
¥Revealing content together at once
默认情况下,Suspense 中的整棵树被视为一个单元。例如,即使这些组件中只有一个挂起等待某些数据,它们也会一起被加载指示器替换:
¥By default, the whole tree inside Suspense is treated as a single unit. For example, even if only one of these components suspends waiting for some data, all of them together will be replaced by the loading indicator:
<Suspense fallback={<Loading />}>
<Biography />
<Panel>
<Albums />
</Panel>
</Suspense>
然后,当它们都准备好显示之后,它们会同时出现在一起。
¥Then, after all of them are ready to be displayed, they will all appear together at once.
在下面的示例中,Biography
和 Albums
都获取了一些数据。但是,因为它们被分组在一个 Suspense 边界下,所以这些组件总是同时一起 “弹出”。
¥In the example below, both Biography
and Albums
fetch some data. However, because they are grouped under a single Suspense boundary, these components always “pop in” together at the same time.
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 边界,因此它们的显示是协调在一起的。
¥Components that load data don’t have to be direct children of the Suspense boundary. For example, you can move Biography
and Albums
into a new Details
component. This doesn’t change the behavior. Biography
and Albums
share the same closest parent Suspense boundary, so their reveal is coordinated together.
<Suspense fallback={<Loading />}>
<Details artistId={artist.id} />
</Suspense>
function Details({ artistId }) {
return (
<>
<Biography artistId={artistId} />
<Panel>
<Albums artistId={artistId} />
</Panel>
</>
);
}
在加载时显示嵌套内容
¥Revealing nested content as it loads
当一个组件挂起时,最近的父 Suspense 组件显示回退。这使你可以嵌套多个 Suspense 组件以创建加载序列。当下一级内容可用时,每个 Suspense 边界的回退将被填充。例如,你可以为专辑列表提供自己的回退:
¥When a component suspends, the closest parent Suspense component shows the fallback. This lets you nest multiple Suspense components to create a loading sequence. Each Suspense boundary’s fallback will be filled in as the next level of content becomes available. For example, you can give the album list its own fallback:
<Suspense fallback={<BigSpinner />}>
<Biography />
<Suspense fallback={<AlbumsGlimmer />}>
<Panel>
<Albums />
</Panel>
</Suspense>
</Suspense>
通过此更改,显示 Biography
不需要 “等待” Albums
的加载。
¥With this change, displaying the Biography
doesn’t need to “wait” for the Albums
to load.
顺序将是:
¥The sequence will be:
-
如果
Biography
尚未加载,则显示BigSpinner
以代替整个内容区域。¥If
Biography
hasn’t loaded yet,BigSpinner
is shown in place of the entire content area. -
Biography
完成加载后,BigSpinner
将被内容替换。¥Once
Biography
finishes loading,BigSpinner
is replaced by the content. -
如果
Albums
尚未加载,则显示AlbumsGlimmer
以代替Albums
及其父级Panel
。¥If
Albums
hasn’t loaded yet,AlbumsGlimmer
is shown in place ofAlbums
and its parentPanel
. -
最后,一旦
Albums
完成加载,它就会替换AlbumsGlimmer
。¥Finally, once
Albums
finishes loading, it replacesAlbumsGlimmer
.
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 boundaries let you coordinate which parts of your UI should always “pop in” together at the same time, and which parts should progressively reveal more content in a sequence of loading states. You can add, move, or delete Suspense boundaries in any place in the tree without affecting the rest of your app’s behavior.
不要在每个组件周围放置一个 Suspense 边界。Suspense 边界不应比你希望用户体验的加载顺序更细化。如果你与设计师合作,请询问他们应该将加载状态放置在哪里 - 他们很可能已经将它们包含在他们的设计线框图中。
¥Don’t put a Suspense boundary around every component. Suspense boundaries should not be more granular than the loading sequence that you want the user to experience. If you work with a designer, ask them where the loading states should be placed—it’s likely that they’ve already included them in their design wireframes.
在加载新内容时显示旧内容
¥Showing stale content while fresh content is loading
在此示例中,SearchResults
组件在获取搜索结果时挂起。键入 "a"
,等待结果,然后将其编辑为 "ab"
。"a"
的结果将被加载回退替换。
¥In this example, the SearchResults
component suspends while fetching the search results. Type "a"
, wait for the results, and then edit it to "ab"
. The results for "a"
will get replaced by the loading fallback.
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
钩子允许你向下传递查询的延迟版本:
¥A common alternative UI pattern is to defer updating the list and to keep showing the previous results until the new results are ready. The useDeferredValue
Hook lets you pass a deferred version of the query down:
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
将显示旧的结果。
¥The query
will update immediately, so the input will display the new value. However, the deferredQuery
will keep its previous value until the data has loaded, so SearchResults
will show the stale results for a bit.
为了让用户更明显,你可以在显示旧的结果列表时添加视觉指示:
¥To make it more obvious to the user, you can add a visual indication when the stale result list is displayed:
<div style={{
opacity: query !== deferredQuery ? 0.5 : 1
}}>
<SearchResults query={deferredQuery} />
</div>
在下例中输入 "a"
,等待结果加载,然后编辑输入为 "ab"
。请注意,在加载新结果之前,你现在看到的不是 Suspense 回退,而是变暗的旧结果列表:
¥Enter "a"
in the example below, wait for the results to load, and then edit the input to "ab"
. Notice how instead of the Suspense fallback, you now see the dimmed stale result list until the new results have loaded:
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> </> ); }
防止隐藏已经显示的内容
¥Preventing already revealed content from hiding
当组件挂起时,最近的父 Suspense 边界切换为显示回退。如果它已经显示了一些内容,这可能会导致不和谐的用户体验。尝试按下这个按钮:
¥When a component suspends, the closest parent Suspense boundary switches to showing the fallback. This can lead to a jarring user experience if it was already displaying some content. Try pressing this button:
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
取代。
¥When you pressed the button, the Router
component rendered ArtistPage
instead of IndexPage
. A component inside ArtistPage
suspended, so the closest Suspense boundary started showing the fallback. The closest Suspense boundary was near the root, so the whole site layout got replaced by BigSpinner
.
要防止这种情况,你可以使用 startTransition
: 将导航状态更新标记为转换
¥To prevent this, you can mark the navigation state update as a Transition with startTransition
:
function Router() {
const [page, setPage] = useState('/');
function navigate(url) {
startTransition(() => {
setPage(url);
});
}
// ...
这告诉 React 状态转场并不紧急,最好继续显示上一页而不是隐藏任何已经显示的内容。现在点击按钮 “等待” 加载 Biography
:
¥This tells React that the state transition is not urgent, and it’s better to keep showing the previous page instead of hiding any already revealed content. Now clicking the button “waits” for the Biography
to load:
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>; }
Transition 不会等待所有内容加载。它只会等待足够长的时间以避免隐藏已经显示的内容。例如,网站 Layout
已经暴露,因此将其隐藏在加载旋转器后面是不好的。但是,Albums
周围的嵌套 Suspense
边界是新的,因此 Transition 不会等待它。
¥A Transition doesn’t wait for all content to load. It only waits long enough to avoid hiding already revealed content. For example, the website Layout
was already revealed, so it would be bad to hide it behind a loading spinner. However, the nested Suspense
boundary around Albums
is new, so the Transition doesn’t wait for it.
指示转换正在发生
¥Indicating that a Transition is happening
在上面的示例中,单击按钮后,没有视觉指示正在进行导航。要添加指标,你可以将 startTransition
替换为 useTransition
,这将为你提供一个布尔值 isPending
。在下面的示例中,它用于在 Transition 发生时更改网站标题样式:
¥In the above example, once you click the button, there is no visual indication that a navigation is in progress. To add an indicator, you can replace startTransition
with useTransition
which gives you a boolean isPending
value. In the example below, it’s used to change the website header styling while a Transition is happening:
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 边界
¥Resetting Suspense boundaries on navigation
在转换期间,React 将避免隐藏已经显示的内容。但是,如果你导航到具有不同参数的路由,你可能想告诉 React 它是不同的内容。你可以用 key
来表达:
¥During a Transition, React will avoid hiding already revealed content. However, if you navigate to a route with different parameters, you might want to tell React it is different content. You can express this with a key
:
<ProfilePage key={queryParams.id} />
想象一下,你正在用户的个人资料页面中导航,并且某些东西暂停了。如果该更新封装在 Transition 中,则不会触发已可见内容的回退。这是预期的行为。
¥Imagine you’re navigating within a user’s profile page, and something suspends. If that update is wrapped in a Transition, it will not trigger the fallback for already visible content. That’s the expected behavior.
但是,现在假设你正在两个不同的用户配置文件之间导航。在这种情况下,显示回退是有意义的。例如,一个用户的时间线与另一个用户的时间线的内容不同。通过指定 key
,你可以确保 React 将不同用户的个人资料视为不同的组件,并在导航期间重置 Suspense 边界。Suspense 集成路由应该自动执行此操作。
¥However, now imagine you’re navigating between two different user profiles. In that case, it makes sense to show the fallback. For example, one user’s timeline is different content from another user’s timeline. By specifying a key
, you ensure that React treats different users’ profiles as different components, and resets the Suspense boundaries during navigation. Suspense-integrated routers should do this automatically.
为服务器错误和仅限客户端的内容提供回退
¥Providing a fallback for server errors and client-only content
如果你使用 流式服务器渲染 API 之一(或依赖它们的框架),React 也会使用你的 <Suspense>
边界来处理服务器上的错误。如果组件在服务器上抛出错误,React 不会中止服务器渲染。而是,它会在其上方找到最接近的 <Suspense>
组件,并将其回退(例如加载控件)包含到生成的服务器 HTML 中。用户首先会看到一个加载控件。
¥If you use one of the streaming server rendering APIs (or a framework that relies on them), React will also use your <Suspense>
boundaries to handle errors on the server. If a component throws an error on the server, React will not abort the server render. Instead, it will find the closest <Suspense>
component above it and include its fallback (such as a spinner) into the generated server HTML. The user will see a spinner at first.
在客户端,React 将尝试再次渲染相同的组件。如果在客户端也出错,React 会抛出错误并显示最接近的 错误边界。。但是,如果在客户端没有出错,React 不会向用户显示错误,因为内容最终已成功显示。
¥On the client, React will attempt to render the same component again. If it errors on the client too, React will throw the error and display the closest error boundary. However, if it does not error on the client, React will not display the error to the user since the content was eventually displayed successfully.
你可以使用它来选择不在服务器上渲染某些组件。为此,在服务器环境中抛出一个错误,然后将它们封装在 <Suspense>
边界中,以用回退替换它们的 HTML:
¥You can use this to opt out some components from rendering on the server. To do this, throw an error in the server environment and then wrap them in a <Suspense>
boundary to replace their HTML with fallbacks:
<Suspense fallback={<Loading />}>
<Chat />
</Suspense>
function Chat() {
if (typeof window === 'undefined') {
throw Error('Chat should only render on the client.');
}
// ...
}
服务器 HTML 将包含加载指示器。它将被客户端上的 Chat
组件替换。
¥The server HTML will include the loading indicator. It will be replaced by the Chat
component on the client.
故障排除
¥Troubleshooting
如何防止 UI 在更新期间被回退替换?
¥How do I prevent the UI from being replaced by a fallback during an update?
用回退替换可见的 UI 会产生不和谐的用户体验。当更新导致组件挂起并且最近的 Suspense 边界已经向用户显示内容时,就会发生这种情况。
¥Replacing visible UI with a fallback creates a jarring user experience. This can happen when an update causes a component to suspend, and the nearest Suspense boundary is already showing content to the user.
为了防止这种情况发生,使用 startTransition
将更新标记为非紧急。在转换期间,React 将等到加载了足够的数据以防止出现不必要的回退:
¥To prevent this from happening, mark the update as non-urgent using startTransition
. During a Transition, React will wait until enough data has loaded to prevent an unwanted fallback from appearing:
function handleNextPageClick() {
// If this update suspends, don't hide the already displayed content
startTransition(() => {
setCurrentPage(currentPage + 1);
});
}
这将避免隐藏现有内容。但是,任何新渲染的 Suspense
边界仍将立即显示回退以避免阻塞 UI 并让用户在内容可用时看到内容。
¥This will avoid hiding existing content. However, any newly rendered Suspense
boundaries will still immediately display fallbacks to avoid blocking the UI and let the user see the content as it becomes available.
React 只会在非紧急更新期间防止不必要的回退。如果它是紧急更新的结果,它不会延迟渲染。你必须选择使用 startTransition
或 useDeferredValue
之类的 API。
¥React will only prevent unwanted fallbacks during non-urgent updates. It will not delay a render if it’s the result of an urgent update. You must opt in with an API like startTransition
or useDeferredValue
.
如果你的路由与 Suspense 集成,它应该自动将其更新封装到 startTransition
中。
¥If your router is integrated with Suspense, it should wrap its updates into startTransition
automatically.