useTransition

useTransition 是一个帮助你在不阻塞 UI 的情况下更新状态的 React Hook。

const [isPending, startTransition] = useTransition()

参考

useTransition()

在组件顶层调用 useTransition,将某些状态更新标记为 transition。

import { useTransition } from 'react';

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

参见下方更多示例

参数

useTransition 不需要任何参数。

返回值

useTransition 返回一个由两个元素组成的数组:

  1. isPending,告诉你是否存在待处理的 transition。
  2. startTransition 函数,你可以使用此方法将状态更新标记为 transition。

startTransition 函数

useTransition 返回的 startTransition 函数允许你将状态更新标记为 transition。

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

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

参数

  • 作用域(scope):一个通过调用一个或多个 set 函数 更新状态的函数。React 会立即不带参数地调用此函数,并将在 scope 调用期间将所有同步安排的状态更新标记为 transition。它们将是非阻塞的,并且 不会显示不想要的加载指示器

返回值

startTransition 不返回任何值。

注意

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

  • 只有在可以访问该状态的 set 函数时,才能将其对应的状态更新包装为 transition。如果你想启用 Transition 以响应某个 prop 或自定义 Hook 值,请尝试使用 useDeferredValue

  • 传递给 startTransition 的函数必须是同步的。React 会立即执行此函数,并将在其执行期间发生的所有状态更新标记为 transition。如果在其执行期间,尝试稍后执行状态更新(例如在一个定时器中执行状态更新),这些状态更新不会被标记为 transition。

  • 标记为 Transition 的状态更新将被其他状态更新打断。例如在 Transition 中更新图表组件,并在图表组件仍在重新渲染时继续在输入框中输入,React 将首先处理输入框的更新,之后再重新启动对图表组件的渲染工作。

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

  • 目前,React 会批处理多个同时进行的 transition。这是一个限制,可能会在未来版本中删除。


用法

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

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

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

transition 可以使用户界面的更新在慢速设备上仍保持响应性。

通过 transition,UI 仍将在重新渲染过程中保持响应性。例如用户点击一个选项卡,但改变了主意并点击另一个选项卡,他们可以在不等待第一个重新渲染完成的情况下完成操作。

使用 useTransition 与常规状态更新的区别

1示例 2 个挑战:
在 Transition 中更新当前选项卡

在此示例中,“文章”选项卡被 人为地减慢,以便至少需要一秒钟才能渲染。

点击“Posts”,然后立即点击“Contact”。请注意,这会中断“Posts”的缓慢渲染,而“联系人”选项卡将会立即显示。因为此状态更新被标记为 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 组件在 Transition 中包装了 onClick 逻辑:

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。这就是为什么可以在点击“Posts”后立即点击“Contact”。由于更新选定选项卡被标记为了 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 中。例如,选项卡按钮可以有一个特殊的“pending”视觉状态:

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

请注意,现在点击“Posts”感觉更加灵敏,因为选项卡按钮本身立即更新了:

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 的数据源中获取了一些数据。当你点击“Posts”选项卡时,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>
  );
}

了解有关在Suspense中使用转换的更多信息

注意

转换效果只会“等待”足够长的时间来避免隐藏 已经显示 的内容(例如选项卡容器)。如果“帖子”选项卡具有一个嵌套 <Suspense> 边界,转换效果将不会“等待”它。


构建一个Suspense-enabled 的路由

如果你正在构建一个 React 框架或路由,我们建议将页面导航标记为转换效果。

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

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

这么做有两个好处:

下面是一个简单的使用转换效果进行页面导航的路由器示例:

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 的路由默认情况下会将页面导航更新包装为 transition。


Displaying an error to users with an error boundary

Canary

Error Boundary for useTransition is currently only available in React’s canary and experimental channels. Learn more about React’s release channels here.

If a function passed to startTransition throws an error, you can display an error to your user with an error boundary. To use an error boundary, wrap the component where you are calling the useTransition in an error boundary. Once the function passed to startTransition errors, the fallback for the error boundary will be displayed.

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


Troubleshooting

在 Transition 中无法更新输入框内容

不应将控制输入框的状态变量标记为 transition:

const [text, setText] = useState('');
// ...
function handleChange(e) {
// ❌ 不应将受控输入框的状态变量标记为 Transition
startTransition(() => {
setText(e.target.value);
});
}
// ...
return <input value={text} onChange={handleChange} />;

这是因为 Transition 是非阻塞的,但是在响应更改事件时更新输入应该是同步的。如果想在输入时运行一个 transition,那么有两种做法:

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

React 没有将状态更新视为 Transition

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

startTransition(() => {
// ✅ 在调用 startTransition 中更新状态
setPage('/about');
});

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

你不能像这样将更新标记为 transition:

startTransition(() => {
// ❌ 在调用 startTransition 后更新状态
setTimeout(() => {
setPage('/about');
}, 1000);
});

相反,你可以这样做:

setTimeout(() => {
startTransition(() => {
// ✅ 在调用 startTransition 中更新状态
setPage('/about');
});
}, 1000);

类似地,你不能像这样将更新标记为 transition:

startTransition(async () => {
await someAsyncFunction();
// ❌ 在调用 startTransition 后更新状态
setPage('/about');
});

然而,使用以下方法可以正常工作:

await someAsyncFunction();
startTransition(() => {
// ✅ 在调用 startTransition 中更新状态
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。你可以将其想象为以下方式:

// React 运行的简易版本

let isInsideTransition = false;

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

function setState() {
if (isInsideTransition) {
// ……安排 Transition 状态更新……
} else {
// ……安排紧急状态更新……
}
}