useTransition

useTransition 是一个 React 钩子,可让你在不阻塞 UI 的情况下更新状态。

¥useTransition is a React Hook that lets you update the state without blocking the UI.

const [isPending, startTransition] = useTransition()

参考

¥Reference

useTransition()

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

¥Call useTransition at the top level of your component to mark some state updates as Transitions.

import { useTransition } from 'react';

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

请参阅下面的更多示例。

¥See more examples below.

参数

¥Parameters

useTransition 没有参数。

¥useTransition does not take any parameters.

返回

¥Returns

useTransition 返回一个恰好包含两项的数组:

¥useTransition returns an array with exactly two items:

  1. isPending 标志告诉你是否有待处理的转换。

    ¥The isPending flag that tells you whether there is a pending Transition.

  2. startTransition 函数 允许你将状态更新标记为转换。

    ¥The startTransition function that lets you mark a state update as a Transition.


startTransition 函数

¥startTransition function

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

¥The startTransition function returned by useTransition lets you mark a state update as a Transition.

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

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

参数

¥Parameters

返回

¥Returns

startTransition 不返回任何东西。

¥startTransition does not return anything.

注意事项

¥Caveats

  • useTransition 是一个钩子,只能在组件内部或者自定义的钩子中调用。如果你需要在其他地方启动 Transition(例如,从数据库中启动),请改为调用独立的 startTransition

    ¥useTransition is a Hook, so it can only be called inside components or custom Hooks. If you need to start a Transition somewhere else (for example, from a data library), call the standalone startTransition instead.

  • 只有当你有权访问该状态的 set 函数时,你才能将更新封装到转换中。如果你想在响应某些属性或自定义钩子值时启动 Transition,请改为尝试 useDeferredValue

    ¥You can wrap an update into a Transition only if you have access to the set function of that state. If you want to start a Transition in response to some prop or a custom Hook value, try useDeferredValue instead.

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

    ¥The function you pass to startTransition must be synchronous. React immediately executes this function, marking all state updates that happen while it executes as Transitions. If you try to perform more state updates later (for example, in a timeout), they won’t be marked as Transitions.

  • startTransition 函数具有稳定的标识,因此你经常会看到它从效果依赖中省略,但包含它不会导致效果触发。如果 linter 允许你在没有错误的情况下省略依赖,那么这样做是安全的。详细了解如何删除副作用依赖。

    ¥The startTransition function has a stable identity, so you will often see it omitted from effect dependencies, but including it will not cause the effect to fire. If the linter lets you omit a dependency without errors, it is safe to do. Learn more about removing Effect dependencies.

  • 标记为 Transition 的状态更新将被其他状态更新打断。例如,如果你在转换中更新图表组件,但在图表处于重新渲染过程中时开始输入输入,则 React 将在处理输入更新后重新启动图表组件上的渲染工作。

    ¥A state update marked as a Transition will be interrupted by other state updates. For example, if you update a chart component inside a Transition, but then start typing into an input while the chart is in the middle of a re-render, React will restart the rendering work on the chart component after handling the input update.

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

    ¥Transition updates can’t be used to control text inputs.

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

    ¥If there are multiple ongoing Transitions, React currently batches them together. This is a limitation that will likely be removed in a future release.


用法

¥Usage

将状态更新标记为非阻塞转换

¥Marking a state update as a non-blocking Transition

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

¥Call useTransition at the top level of your component to mark state updates as non-blocking Transitions.

import { useState, useTransition } from 'react';

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

useTransition 返回一个恰好包含两项的数组:

¥useTransition returns an array with exactly two items:

  1. isPending 标志 告诉你是否有待处理的 Transition。

    ¥The isPending flag that tells you whether there is a pending Transition.

  2. startTransition 函数 可让你将状态更新标记为 Transition。

    ¥The startTransition function that lets you mark a state update as a Transition.

然后,你可以像这样将状态更新标记为转换:

¥You can then mark a state update as a Transition like this:

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

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

转场让你即使在速度较慢的设备上也能保持用户界面更新的响应。

¥Transitions let you keep the user interface updates responsive even on slow devices.

使用转换,你的 UI 在重新渲染过程中保持响应。例如,如果用户单击一个选项卡但随后改变主意并单击另一个选项卡,则它们可以在不等待第一次重新渲染完成的情况下执行此操作。

¥With a Transition, your UI stays responsive in the middle of a re-render. For example, if the user clicks a tab but then change their mind and click another tab, they can do that without waiting for the first re-render to finish.

The difference between useTransition and regular state updates

例子 1 / 2:
在 Transition 中更新当前选项卡

¥Updating the current tab in a Transition

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

¥In this example, the “Posts” tab is artificially slowed down so that it takes at least a second to render.

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

¥Click “Posts” and then immediately click “Contact”. Notice that this interrupts the slow render of “Posts”. The “Contact” tab shows immediately. Because this state update is marked as a Transition, a slow re-render did not freeze the user interface.

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 中更新父组件

¥Updating the parent component in a Transition

你也可以通过 useTransition 调用更新父组件的状态。例如,此 TabButton 组件将其 onClick 逻辑封装在 Transition 中:

¥You can update a parent component’s state from the useTransition call, too. For example, this TabButton component wraps its onClick logic in a 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 事件处理程序中更新其状态,因此该状态更新被标记为转换。这就是为什么,就像在前面的示例中一样,你可以单击 “帖子”,然后立即单击 “联系”。更新选定的选项卡被标记为转换,因此它不会阻止用户交互。

¥Because the parent component updates its state inside the onClick event handler, that state update gets marked as a Transition. This is why, like in the earlier example, you can click on “Posts” and then immediately click “Contact”. Updating the selected tab is marked as a Transition, so it does not block user interactions.

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


在转换期间显示待处理的视觉状态

¥Displaying a pending visual state during the Transition

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

¥You can use the isPending boolean value returned by useTransition to indicate to the user that a Transition is in progress. For example, the tab button can have a special “pending” visual state:

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

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

¥Notice how clicking “Posts” now feels more responsive because the tab button itself updates right away:

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


防止不需要的加载指示器

¥Preventing unwanted loading indicators

在此示例中,PostsTab 组件使用 启用 Suspense 数据源获取一些数据。当你单击 “帖子” 选项卡时,PostsTab 组件会暂停,从而导致出现最接近的加载回退:

¥In this example, the PostsTab component fetches some data using a Suspense-enabled data source. When you click the “Posts” tab, the PostsTab component suspends, causing the closest loading fallback to appear:

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,则可以改为指示在选项卡按钮中显示挂起状态。

¥Hiding the entire tab container to show a loading indicator leads to a jarring user experience. If you add useTransition to TabButton, you can instead indicate display the pending state in the tab button instead.

请注意,单击 “帖子” 不再将整个选项卡容器替换为加载控件:

¥Notice that clicking “Posts” no longer replaces the entire tab container with a spinner:

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 结合使用的更多信息。

¥Read more about using Transitions with Suspense.

注意

转场将只有 “等待” 足够长以避免隐藏已经显示的内容(如选项卡容器)。如果 Posts 选项卡有 嵌套 <Suspense> 边界,则 Transition 不会为其 “等待”。

¥Transitions will only “wait” long enough to avoid hiding already revealed content (like the tab container). If the Posts tab had a nested <Suspense> boundary, the Transition would not “wait” for it.


构建一个启用 Suspense 的路由

¥Building a Suspense-enabled router

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

¥If you’re building a React framework or a router, we recommend marking page navigations as Transitions.

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

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

推荐这样做有两个原因:

¥This is recommended for two reasons:

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

¥Here is a tiny simplified router example using Transitions for navigations.

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 路由预计默认将导航更新封装到转换中。

¥Suspense-enabled routers are expected to wrap the navigation updates into Transitions by default.


向用户显示带有错误边界的错误

¥Displaying an error to users with an error boundary

Canary

useTransition 的错误边界目前仅在 React 的 canary 和实验通道中可用。了解有关 React 的发布渠道在这里 的更多信息。

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

如果传递给 startTransition 的函数抛出错误,你可以使用 误差边界 向用户显示错误。要使用错误边界,请将调用 useTransition 的组件封装在错误边界中。一旦函数传递到 startTransition 错误,就会显示错误边界的回退。

¥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 中更新输入不起作用

¥Updating an input in a Transition doesn’t work

你不能将转换用于控制输入的状态变量:

¥You can’t use a Transition for a state variable that controls an input:

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,你有两个选择:

¥This is because Transitions are non-blocking, but updating an input in response to the change event should happen synchronously. If you want to run a Transition in response to typing, you have two options:

  1. 你可以声明两个单独的状态变量:一个用于输入状态(始终同步更新),另一个用于在 Transition 中更新。这允许你使用同步状态控制输入,并将转换状态变量(将 “滞后” 输入)传递给其余渲染逻辑。

    ¥You can declare two separate state variables: one for the input state (which always updates synchronously), and one that you will update in a Transition. This lets you control the input using the synchronous state, and pass the Transition state variable (which will “lag behind” the input) to the rest of your rendering logic.

  2. 或者,你可以有一个状态变量,并添加 useDeferredValue,这将使 “滞后” 成为实际值。它将使用新值自动触发对 “赶上” 的非阻塞重新渲染。

    ¥Alternatively, you can have one state variable, and add useDeferredValue which will “lag behind” the real value. It will trigger non-blocking re-renders to “catch up” with the new value automatically.


React 不会将我的状态更新视为转换

¥React doesn’t treat my state update as a Transition

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

¥When you wrap a state update in a Transition, make sure that it happens during the startTransition call:

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

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

¥The function you pass to startTransition must be synchronous.

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

¥You can’t mark an update as a Transition like this:

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

而是,你可以这样做:

¥Instead, you could do this:

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

同样,你不能像这样将更新标记为转换:

¥Similarly, you can’t mark an update as a Transition like this:

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

但是,改成这样是有效的:

¥However, this works instead:

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

我想从组件外部调用 useTransition

¥I want to call useTransition from outside a component

你不能在组件外部调用 useTransition,因为它是一个钩子。在这种情况下,请改用独立的 startTransition 方法。它的工作方式相同,但不提供 isPending 指标。

¥You can’t call useTransition outside a component because it’s a Hook. In this case, use the standalone startTransition method instead. It works the same way, but it doesn’t provide the isPending indicator.


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

¥The function I pass to startTransition executes immediately

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

¥If you run this code, it will print 1, 2, 3:

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

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

¥It is expected to print 1, 2, 3. The function you pass to startTransition does not get delayed. Unlike with the browser setTimeout, it does not run the callback later. React executes your function immediately, but any state updates scheduled while it is running are marked as Transitions. You can imagine that it works like this:

// 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 ...
}
}

React 中文网 - 粤ICP备13048890号