<Suspense> 允许你在其子元素加载完成之前显示一个回退内容。

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

参考

🌐 Reference

<Suspense>

属性

🌐 Props

  • children:你打算渲染的实际用户界面。如果 children 在渲染时暂停,Suspense 边界将切换到渲染 fallback
  • fallback:如果实际 UI 尚未加载完成,可使用备用 UI 进行渲染。任何有效的 React 节点都可以,但实际上,fallback 通常是一个轻量级的占位视图,例如加载指示器或骨架屏。Suspense 会在 children 暂停时自动切换到 fallback,并在数据准备好后切换回 children。如果 fallback 在渲染过程中暂停,它将激活最近的父级 Suspense 边界。

注意事项

🌐 Caveats

  • React 不会为那些在首次挂载之前就被挂起的渲染保留任何状态。当组件加载时,React 将从头开始重新尝试渲染被挂起的树。
  • 如果 Suspense 正在为该树显示内容,但随后又挂起,除非导致它的更新是由 startTransitionuseDeferredValue 引起的,否则 fallback 将再次显示。
  • 如果 React 需要隐藏已经可见的内容,因为它再次挂起,它将清理内容树中的布局 Effect。当内容准备好再次显示时,React 将再次触发布局 Effect。这确保了测量 DOM 布局的 Effect 不会在内容被隐藏时尝试执行此操作。
  • React 包含像 流式服务器渲染选择性水化 这样的底层优化,这些优化与 Suspense 集成在一起。阅读 架构概览 并观看 技术讲座 以了解更多信息。

用法

🌐 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 将显示你的 加载回退 ,直到 子组件 所需的所有代码和数据都已加载完成。

在下面的示例中,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>;
}

注意

只有启用 Suspense 的数据源才会激活 Suspense 组件。 它们包括:

  • 使用支持 Suspense 的框架(如 RelayNext.js)进行数据获取
  • 使用 lazy 懒加载组件代码
  • 使用 use 读取缓存的 Promise 的值

Suspense 不会检测在 Effect 或事件处理程序中获取的数据。

🌐 Suspense does not detect when data is fetched inside an Effect or event handler.

在上面的 Albums 组件中加载数据的具体方式取决于你的框架。如果你使用支持 Suspense 的框架,你可以在其数据获取文档中找到详细信息。

🌐 The exact way you would load data in the Albums component above depends on your framework. If you use a Suspense-enabled framework, you’ll find the details in its data fetching documentation.

在不使用有特定意见的框架的情况下,启用 Suspense 的数据获取暂不受支持。实现启用 Suspense 的数据源的要求不稳定且未有文档记录。用于将数据源与 Suspense 集成的官方 API 将在 React 的未来版本中发布。

🌐 Suspense-enabled data fetching without the use of an opinionated framework is not yet supported. The requirements for implementing a Suspense-enabled data source are unstable and undocumented. An official API for integrating data sources with Suspense will be released in a future version of React.


一起显示内容

🌐 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.

在下面的示例中,BiographyAlbums 都会获取一些数据。然而,由于它们被分组在同一个 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 边界的直接子组件。例如,你可以将 BiographyAlbums 移动到一个新的 Details 组件中。这不会改变行为。BiographyAlbums 共享相同的最近父级 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:

  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 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 Hook 让你可以传递查询的延迟版本下去:

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

注意

延迟值和过渡都可以让你避免显示 Suspense 回退,而使用内联指示器。过渡将整个更新标记为非紧急,因此它们通常被框架和路由库用于导航。另一方面,延迟值主要在应用代码中有用,当你想将 UI 的一部分标记为非紧急,并让它“落后”于 UI 的其余部分时使用。

🌐 Both deferred values and Transitions let you avoid showing Suspense fallback in favor of inline indicators. Transitions mark the whole update as non-urgent so they are typically used by frameworks and router libraries for navigation. Deferred values, on the other hand, are mostly useful in application code where you want to mark a part of UI as non-urgent and let it “lag behind” the rest of the UI.


防止隐藏已经显示的内容

🌐 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 而不是 IndexPageArtistPage 内的一个组件挂起了,因此最近的 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>;
}

过渡不会等待所有内容加载完成。它只会等待足够的时间以避免隐藏已经显示的内容。例如,网站 Layout 已经显示出来,所以将它隐藏在加载动画后面是不合适的。然而,围绕 Albums 的嵌套 Suspense 边界是新的,因此过渡不会等待它。

🌐 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.

注意

启用 Suspense 的路由预计默认将导航更新封装到 Transition 中。

🌐 Suspense-enabled routers are expected to wrap the navigation updates into Transitions by default.


指示转换正在发生

🌐 Indicating that a Transition is happening

在上面的示例中,一旦你点击按钮,没有视觉提示表明导航正在进行。要添加指示器,你可以将 startTransition 替换为 useTransition,它会给你一个布尔值 isPending。在下面的示例中,它被用来在过渡进行时改变网站头部的样式:

🌐 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?

用回退内容替换可见的用户界面会造成突兀的用户体验。当更新导致某个组件挂起,而最近的 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 边界仍然会立即显示备用内容,以避免阻塞用户界面,并让用户在内容变得可用时即可看到它。

🌐 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 只会在非紧急更新期间阻止不必要的回退。如果渲染是由紧急更新引起的,它不会延迟渲染。你必须使用像 startTransitionuseDeferredValue 这样的 API 来选择启用。

如果你的路由集成了 Suspense,它应该会自动将其更新封装到 startTransition 中。

🌐 If your router is integrated with Suspense, it should wrap its updates into startTransition automatically.