<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. If children suspends while rendering, the Suspense boundary will switch to rendering fallback.

  • 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 to fallback when children suspends, and back to children when the data is ready. If fallback 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 将再次显示,除非导致它的更新是由 startTransitionuseDeferredValue 引起的。

    ¥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 by startTransition or useDeferredValue.

  • 如果 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>;
}

注意

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

¥Only Suspense-enabled data sources will activate the Suspense component. They include:

  • 使用支持 Suspense 的框架(如 RelayNext.js)获取数据

    ¥Data fetching with Suspense-enabled frameworks like Relay and Next.js

  • 使用 lazy 延迟加载组件代码

    ¥Lazy-loading component code with lazy

  • 使用 use 读取 Promise 的值

    ¥Reading the value of a Promise with use

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

¥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 以代替整个内容区域。

    ¥If Biography hasn’t loaded yet, BigSpinner is shown in place of the entire content area.

  2. Biography 完成加载后,BigSpinner 将被内容替换。

    ¥Once Biography finishes loading, BigSpinner is replaced by the content.

  3. 如果 Albums 尚未加载,则显示 AlbumsGlimmer 以代替 Albums 及其父级 Panel

    ¥If Albums hasn’t loaded yet, AlbumsGlimmer is shown in place of Albums and its parent Panel.

  4. 最后,一旦 Albums 完成加载,它就会替换 AlbumsGlimmer

    ¥Finally, once Albums finishes loading, it replaces 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 钩子允许你向下传递查询的延迟版本:

¥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 回退。Transitions 将整个更新标记为非紧急更新,因此它们通常被框架和路由库用于导航。另一方面,延迟值在你想要将 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>;
}

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.

注意

启用 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。在下面的示例中,它用于在 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 只会在非紧急更新期间防止不必要的回退。如果它是紧急更新的结果,它不会延迟渲染。你必须选择使用 startTransitionuseDeferredValue 之类的 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.


React 中文网 - 粤ICP备13048890号