<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 需要隐藏已经可见的内容,因为它再次暂停,它将清除内容树中的 布局 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 组件。 它们包括

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

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

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

尚不支持在不使用规范性框架的情况下进行启用 Suspense 的数据获取。实现启用 Suspense 的数据源的要求是不稳定的,并且没有文档记录。在未来版本的 React 中将发布用于将数据源与 Suspense 集成的官方 API。


一次性显示所有内容

默认情况下,Suspense 内部的整个树都被视为一个单元。例如,即使这些组件中_只有一个_暂停等待某些数据,_所有_组件都将被加载指示器替换

<Suspense fallback={<Loading />}>
<Biography />
<Panel>
<Albums />
</Panel>
</Suspense>

然后,在所有组件都准备好显示后,它们将立即一起显示。

在下面的示例中,BiographyAlbums 都获取了一些数据。但是,因为它们被分组在一个 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 边界的直接子组件。例如,您可以将 BiographyAlbums 移至新的 Details 组件中。这不会改变行为。BiographyAlbums 共享同一个最近的父 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 钩子允许你传递一个延迟版本的查询

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 中。