useTransition

useTransition 是一个 React Hook,它允许你在不阻塞 UI 的情况下更新状态。

const [isPending, startTransition] = useTransition()

参考

useTransition()

在组件的顶层调用 useTransition 来将某些状态更新标记为过渡。

import { useTransition } from 'react';

function TabContainer() {
const [isPending, startTransition] = useTransition();
// ...
}

请参阅下面的更多示例。

参数

useTransition 不接受任何参数。

返回值

useTransition 返回一个包含两个项目的数组

  1. isPending 标志,告诉你是否有一个待处理的过渡。
  2. startTransition 函数,它允许你将状态更新标记为过渡。

startTransition 函数

useTransition 返回的 startTransition 函数允许您将状态更新标记为 Transition(过渡)。

function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('about');

function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
// ...
}

参数

返回值

startTransition 不返回任何内容。

注意事项

  • useTransition 是一个 Hook,因此只能在组件或自定义 Hook 内部调用。如果您需要在其他地方(例如,从数据库)启动 Transition,请调用独立的 startTransition

  • 只有在您可以访问该状态的 set 函数的情况下,才能将更新包装到 Transition 中。如果您想响应某些 prop 或自定义 Hook 值来启动 Transition,请尝试使用 useDeferredValue

  • 您传递给 startTransition 的函数必须是同步的。React 会立即执行此函数,并将执行期间发生的所有状态更新标记为 Transitions。如果您尝试稍后执行更多状态更新(例如,在超时中),则它们不会被标记为 Transitions。

  • 标记为 Transition 的状态更新将被其他状态更新中断。例如,如果您在 Transition 中更新了图表组件,但在图表处于重新渲染过程中开始在输入框中键入内容,则 React 会在处理输入更新后重新启动图表组件上的渲染工作。

  • Transition 更新不能用于控制文本输入。

  • 如果有多个正在进行的 Transitions,React 目前会将它们一起批处理。这是一个限制,可能会在未来的版本中删除。


用法

将状态更新标记为非阻塞 Transition

在组件的顶层调用 useTransition,以将状态更新标记为非阻塞的*Transitions*。

import { useState, useTransition } from 'react';

function TabContainer() {
const [isPending, startTransition] = useTransition();
// ...
}

useTransition 返回一个包含两个项目的数组

  1. isPending 标志 告诉您是否存在挂起的 Transition。
  2. startTransition 函数 允许您将状态更新标记为 Transition。

然后,您可以像这样将状态更新标记为 Transition:

function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('about');

function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
// ...
}

Transitions 允许您即使在速度较慢的设备上也能保持用户界面更新的响应速度。

使用 Transition,您的 UI 在重新渲染过程中保持响应。例如,如果用户单击了一个选项卡,但随后改变主意并单击了另一个选项卡,则他们无需等待第一次重新渲染完成即可执行此操作。

useTransition 和常规状态更新之间的区别

示例: 1关于 2:
在 Transition 中更新当前选项卡

在此示例中,“帖子”选项卡被**人为地放慢了**,因此渲染至少需要一秒钟。

单击“帖子”,然后立即单击“联系我们”。请注意,这会中断“帖子”的缓慢渲染。“联系我们”选项卡会立即显示。因为此状态更新被标记为 Transition,所以缓慢的重新渲染不会冻结用户界面。

import { useState, useTransition } from 'react';
import TabButton from './TabButton.js';
import AboutTab from './AboutTab.js';
import PostsTab from './PostsTab.js';
import ContactTab from './ContactTab.js';

export default function TabContainer() {
  const [isPending, startTransition] = useTransition();
  const [tab, setTab] = useState('about');

  function selectTab(nextTab) {
    startTransition(() => {
      setTab(nextTab);
    });
  }

  return (
    <>
      <TabButton
        isActive={tab === 'about'}
        onClick={() => selectTab('about')}
      >
        About
      </TabButton>
      <TabButton
        isActive={tab === 'posts'}
        onClick={() => selectTab('posts')}
      >
        Posts (slow)
      </TabButton>
      <TabButton
        isActive={tab === 'contact'}
        onClick={() => selectTab('contact')}
      >
        Contact
      </TabButton>
      <hr />
      {tab === 'about' && <AboutTab />}
      {tab === 'posts' && <PostsTab />}
      {tab === 'contact' && <ContactTab />}
    </>
  );
}


在 Transition 中更新父组件

您也可以从 useTransition 调用更新父组件的状态。例如,这个 TabButton 组件将其 onClick 逻辑包装在一个 Transition 中

export default function TabButton({ children, isActive, onClick }) {
const [isPending, startTransition] = useTransition();
if (isActive) {
return <b>{children}</b>
}
return (
<button onClick={() => {
startTransition(() => {
onClick();
});
}}>
{children}
</button>
);
}

因为父组件在 onClick 事件处理程序中更新其状态,所以该状态更新被标记为 Transition。这就是为什么,就像在前面的例子中一样,您可以点击“帖子”,然后立即点击“联系”。更新所选选项卡被标记为 Transition,因此它不会阻塞用户交互。

import { useTransition } from 'react';

export default function TabButton({ children, isActive, onClick }) {
  const [isPending, startTransition] = useTransition();
  if (isActive) {
    return <b>{children}</b>
  }
  return (
    <button onClick={() => {
      startTransition(() => {
        onClick();
      });
    }}>
      {children}
    </button>
  );
}


在 Transition 期间显示挂起视觉状态

您可以使用 useTransition 返回的 isPending 布尔值来向用户指示 Transition 正在进行中。例如,选项卡按钮可以有一个特殊的“挂起”视觉状态

function TabButton({ children, isActive, onClick }) {
const [isPending, startTransition] = useTransition();
// ...
if (isPending) {
return <b className="pending">{children}</b>;
}
// ...

请注意,点击“帖子”现在感觉响应更快,因为选项卡按钮本身会立即更新

import { useTransition } from 'react';

export default function TabButton({ children, isActive, onClick }) {
  const [isPending, startTransition] = useTransition();
  if (isActive) {
    return <b>{children}</b>
  }
  if (isPending) {
    return <b className="pending">{children}</b>;
  }
  return (
    <button onClick={() => {
      startTransition(() => {
        onClick();
      });
    }}>
      {children}
    </button>
  );
}


防止不必要的加载指示器

在这个例子中,PostsTab 组件使用支持 Suspense 的数据源获取一些数据。当您点击“帖子”选项卡时,PostsTab 组件会*挂起*,导致出现最近的加载回退

import { Suspense, useState } from 'react';
import TabButton from './TabButton.js';
import AboutTab from './AboutTab.js';
import PostsTab from './PostsTab.js';
import ContactTab from './ContactTab.js';

export default function TabContainer() {
  const [tab, setTab] = useState('about');
  return (
    <Suspense fallback={<h1>🌀 Loading...</h1>}>
      <TabButton
        isActive={tab === 'about'}
        onClick={() => setTab('about')}
      >
        About
      </TabButton>
      <TabButton
        isActive={tab === 'posts'}
        onClick={() => setTab('posts')}
      >
        Posts
      </TabButton>
      <TabButton
        isActive={tab === 'contact'}
        onClick={() => setTab('contact')}
      >
        Contact
      </TabButton>
      <hr />
      {tab === 'about' && <AboutTab />}
      {tab === 'posts' && <PostsTab />}
      {tab === 'contact' && <ContactTab />}
    </Suspense>
  );
}

隐藏整个选项卡容器以显示加载指示器会导致用户体验不佳。如果将 useTransition 添加到 TabButton 中,则可以改为在选项卡按钮中指示显示挂起状态。

请注意,点击“帖子”不再会用微调器替换整个选项卡容器

import { useTransition } from 'react';

export default function TabButton({ children, isActive, onClick }) {
  const [isPending, startTransition] = useTransition();
  if (isActive) {
    return <b>{children}</b>
  }
  if (isPending) {
    return <b className="pending">{children}</b>;
  }
  return (
    <button onClick={() => {
      startTransition(() => {
        onClick();
      });
    }}>
      {children}
    </button>
  );
}

阅读有关将 Transitions 与 Suspense 一起使用的更多信息。

注意

Transitions 只会“等待”足够长的时间,以避免隐藏*已经显示*的内容(如选项卡容器)。如果“帖子”选项卡有一个嵌套的 <Suspense> 边界,则 Transition 不会“等待”它。


构建支持 Suspense 的路由器

如果您正在构建 React 框架或路由器,我们建议将页面导航标记为 Transitions。

function Router() {
const [page, setPage] = useState('/');
const [isPending, startTransition] = useTransition();

function navigate(url) {
startTransition(() => {
setPage(url);
});
}
// ...

建议这样做有两个原因

下面是一个使用 Transitions 进行导航的小型简化路由器示例。

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 的路由器默认情况下会将导航更新包装到 Transitions 中。


使用错误边界向用户显示错误

Canary

useTransition 的错误边界目前只在 React 的 canary 和实验频道中可用。在此处详细了解React 的发布频道

如果传递给 startTransition 的函数抛出错误,您可以使用 错误边界 向用户显示错误消息。要使用错误边界,请将调用 useTransition 的组件包裹在错误边界中。一旦传递给 startTransition 的函数出错,就会显示错误边界的回退内容。

import { useTransition } from "react";
import { ErrorBoundary } from "react-error-boundary";

export function AddCommentContainer() {
  return (
    <ErrorBoundary fallback={<p>⚠️Something went wrong</p>}>
      <AddCommentButton />
    </ErrorBoundary>
  );
}

function addComment(comment) {
  // For demonstration purposes to show Error Boundary
  if (comment == null) {
    throw new Error("Example Error: An error thrown to trigger error boundary");
  }
}

function AddCommentButton() {
  const [pending, startTransition] = useTransition();

  return (
    <button
      disabled={pending}
      onClick={() => {
        startTransition(() => {
          // Intentionally not passing a comment
          // so error gets thrown
          addComment();
        });
      }}
    >
      Add comment
    </button>
  );
}


故障排除

在 Transition 中更新输入框无效

您不能将 Transition 用于控制输入框的状态变量

const [text, setText] = useState('');
// ...
function handleChange(e) {
// ❌ Can't use Transitions for controlled input state
startTransition(() => {
setText(e.target.value);
});
}
// ...
return <input value={text} onChange={handleChange} />;

这是因为 Transition 是非阻塞的,但响应更改事件更新输入框应该是同步发生的。如果您想在输入时运行 Transition,您有两个选择

  1. 您可以声明两个独立的状态变量:一个用于输入框状态(始终同步更新),一个将在 Transition 中更新。这使您可以使用同步状态控制输入框,并将 Transition 状态变量(将“滞后于”输入框)传递给其余的渲染逻辑。
  2. 或者,您可以使用一个状态变量,并添加 useDeferredValue,它将“滞后于”实际值。它将触发非阻塞的重新渲染,以自动“赶上”新值。

React 没有将我的状态更新视为 Transition

当您将状态更新包裹在 Transition 中时,请确保它发生在 startTransition 调用期间

startTransition(() => {
// ✅ Setting state *during* startTransition call
setPage('/about');
});

您传递给 startTransition 的函数必须是同步的。

您不能像这样将更新标记为 Transition

startTransition(() => {
// ❌ Setting state *after* startTransition call
setTimeout(() => {
setPage('/about');
}, 1000);
});

相反,您可以这样做

setTimeout(() => {
startTransition(() => {
// ✅ Setting state *during* startTransition call
setPage('/about');
});
}, 1000);

类似地,您不能像这样将更新标记为 Transition

startTransition(async () => {
await someAsyncFunction();
// ❌ Setting state *after* startTransition call
setPage('/about');
});

但是,这样做是可行的

await someAsyncFunction();
startTransition(() => {
// ✅ Setting state *during* startTransition call
setPage('/about');
});

我想从组件外部调用 useTransition

您不能在组件外部调用 useTransition,因为它是一个 Hook。在这种情况下,请改用独立的 startTransition 方法。它的工作原理相同,但不提供 isPending 指示器。


我传递给 startTransition 的函数立即执行

如果您运行此代码,它将打印 1、2、3

console.log(1);
startTransition(() => {
console.log(2);
setPage('/about');
});
console.log(3);

预计打印 1、2、3。 您传递给 startTransition 的函数不会被延迟。与浏览器 setTimeout 不同,它不会稍后运行回调。React 会立即执行您的函数,但任何在其运行期间安排的状态更新都会被标记为 Transition。您可以想象它是这样工作的

// A simplified version of how React works

let isInsideTransition = false;

function startTransition(scope) {
isInsideTransition = true;
scope();
isInsideTransition = false;
}

function setState() {
if (isInsideTransition) {
// ... schedule a Transition state update ...
} else {
// ... schedule an urgent state update ...
}
}