<Suspense>

<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需要隐藏已经可见的内容,因为它再次挂起了,它将清理内容树中的布局Effect。当内容准备好再次显示时,React将再次触发布局Effect。这确保了测量DOM布局的Effect不会在内容隐藏时尝试这样做。
  • 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 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>
    </>
  );
}

注意

延迟值和 过渡 都允许你避免显示 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 的路由器应该默认情况下将导航更新包装到过渡中。


指示正在进行过渡

在上面的示例中,一旦你点击按钮,没有任何视觉指示表明导航正在进行中。要添加指示器,你可以用 useTransition 替换 startTransition,它会给你一个布尔值 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 中。