useTransition

useTransition 是一个 React Hook,允许您在后台渲染 UI 的一部分。

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(action)

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 中。如果您想响应某些属性或自定义 Hook 值来启动 Transition,请尝试使用useDeferredValue

  • 您传递给startTransition 的函数会立即被调用,将执行期间发生的所有状态更新标记为 Transitions。例如,如果您尝试在setTimeout中执行状态更新,则它们不会被标记为 Transitions。

  • 您必须在任何异步请求后的任何状态更新中包装另一个startTransition,以将其标记为 Transitions。这是一个已知的限制,我们将在未来修复它(参见故障排除)。

  • startTransition 函数具有稳定的标识,因此您经常会看到它从 Effect 依赖项中省略,但包含它不会导致 Effect 触发。如果 linter 允许您在没有错误的情况下省略依赖项,则可以安全地执行此操作。了解有关删除 Effect 依赖项的更多信息。

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

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

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

用法

使用 Actions 执行非阻塞更新

在组件顶部调用useTransition 来创建 Actions,并访问 pending 状态。

import {useState, useTransition} from 'react';

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

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

  1. 指示是否存在挂起 Transition 的isPending标志
  2. 允许您创建 Action 的startTransition 函数

要启动 Transition,请像这样将函数传递给startTransition

import {useState, useTransition} from 'react';
import {updateQuantity} from './api';

function CheckoutForm() {
const [isPending, startTransition] = useTransition();
const [quantity, setQuantity] = useState(1);

function onSubmit(newQuantity) {
startTransition(async function () {
const savedQuantity = await updateQuantity(newQuantity);
startTransition(() => {
setQuantity(savedQuantity);
});
});
}
// ...
}

传递给startTransition 的函数称为“Action”(动作)。您可以在 Action 中更新状态和(可选)执行副作用,并且工作将在后台完成,而不会阻塞页面上的用户交互。一个 Transition 可以包含多个 Actions,并且在 Transition 正在进行时,您的 UI 保持响应。例如,如果用户点击一个选项卡,然后改变主意并点击另一个选项卡,则第二个点击将立即处理,而无需等待第一个更新完成。

为了向用户提供有关正在进行的 Transitions 的反馈,isPending 状态会在第一次调用startTransition 时切换为true,并保持true 直到所有 Actions 完成并将最终状态显示给用户。Transitions 确保 Actions 中的副作用按顺序完成,以防止不需要的加载指示器,并且您可以在 Transition 正在进行时使用useOptimistic 提供即时反馈。

Action 与常规事件处理的差异

示例 1 2:
在 Action 中更新数量

在这个例子中,updateQuantity 函数模拟了向服务器发送请求以更新购物车中商品数量的请求。此函数被人为地放慢,以便请求至少需要一秒才能完成。

快速多次更新数量。注意,在任何请求正在进行时,都会显示挂起的“总计”状态,并且只有在最终请求完成后,“总计”才会更新。因为更新是在 Action 中进行的,所以在请求进行时,“数量”可以继续更新。

import { useState, useTransition } from "react";
import { updateQuantity } from "./api";
import Item from "./Item";
import Total from "./Total";

export default function App({}) {
  const [quantity, setQuantity] = useState(1);
  const [isPending, startTransition] = useTransition();

  const updateQuantityAction = async newQuantity => {
    // To access the pending state of a transition,
    // call startTransition again.
    startTransition(async () => {
      const savedQuantity = await updateQuantity(newQuantity);
      startTransition(() => {
        setQuantity(savedQuantity);
      });
    });
  };

  return (
    <div>
      <h1>Checkout</h1>
      <Item action={updateQuantityAction}/>
      <hr />
      <Total quantity={quantity} isPending={isPending} />
    </div>
  );
}

这是一个演示 Action 如何工作的基本示例,但此示例未处理请求乱序完成的情况。多次更新数量时,之前的请求可能在后面的请求完成之后完成,导致数量更新顺序错误。这是一个已知的限制,我们将在未来修复(参见下面的故障排除)。

对于常见用例,React 提供了内置的抽象,例如

这些解决方案会为您处理请求顺序。当使用 Transitions 构建您自己的自定义 Hook 或管理异步状态转换的库时,您可以更好地控制请求顺序,但必须自行处理。


公开组件的 action 属性

您可以公开组件的 action 属性,以允许父组件调用 Action。

例如,此 TabButton 组件将其 onClick 逻辑包装在 action 属性中

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

因为父组件在其 action 内部更新其状态,所以该状态更新被标记为 Transition。这意味着您可以先点击“帖子”,然后立即点击“联系”,它不会阻塞用户交互。

import { useTransition } from 'react';

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


显示挂起的可视状态

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

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

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

import { useTransition } from 'react';

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


防止不需要的加载指示器

在这个例子中,PostsTab 组件使用 use 获取一些数据。当您点击“帖子”选项卡时,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'}
        action={() => setTab('about')}
      >
        About
      </TabButton>
      <TabButton
        isActive={tab === 'posts'}
        action={() => setTab('posts')}
      >
        Posts
      </TabButton>
      <TabButton
        isActive={tab === 'contact'}
        action={() => 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({ action, children, isActive }) {
  const [isPending, startTransition] = useTransition();
  if (isActive) {
    return <b>{children}</b>
  }
  if (isPending) {
    return <b className="pending">{children}</b>;
  }
  return (
    <button onClick={() => {
      startTransition(() => {
        action();
      });
    }}>
      {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);
});
}
// ...

建议这样做有三个原因

这是一个使用转换进行导航的简化路由器示例。

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


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

如果传递给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>
  );
}


故障排除

在转换中更新输入不起作用

您不能对控制输入的状态变量使用转换

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

这是因为转换是非阻塞的,但响应更改事件更新输入应该同步进行。如果您想在键入时运行转换,则有两个选项

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

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

当您将状态更新包装在转换中时,请确保它发生在startTransition调用期间。

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

您传递给startTransition的函数必须是同步的。您不能像这样将更新标记为转换

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

相反,您可以这样做

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

React 没有将我await后的状态更新视为转换

当您在startTransition函数内使用await时,await之后的 state 更新不会被标记为转换。您必须将每次await之后的 state 更新包装在startTransition调用中。

startTransition(async () => {
await someAsyncFunction();
// ❌ Not using startTransition after await
setPage('/about');
});

但是,这样做可以解决问题

startTransition(async () => {
await someAsyncFunction();
// ✅ Using startTransition *after* await
startTransition(() => {
setPage('/about');
});
});

这是由于 React 丢失了异步上下文的作用域而导致的 JavaScript 限制。将来,当AsyncContext可用时,此限制将被消除。


我想在组件外部调用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 会立即执行您的函数,但是在函数运行期间安排的任何状态更新都会被标记为转换。您可以这样理解它的工作原理:

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

我的转换中的状态更新顺序不对

如果在 startTransition 内部使用 await,您可能会看到更新顺序出错。

在这个例子中,updateQuantity 函数模拟了向服务器发送请求以更新购物车中商品数量的操作。此函数人为地使每次请求都延后上一次请求完成之后再返回,以模拟网络请求的竞争条件。

尝试更新一次数量,然后快速多次更新它。您可能会看到不正确的总数。

import { useState, useTransition } from "react";
import { updateQuantity } from "./api";
import Item from "./Item";
import Total from "./Total";

export default function App({}) {
  const [quantity, setQuantity] = useState(1);
  const [isPending, startTransition] = useTransition();
  // Store the actual quantity in separate state to show the mismatch.
  const [clientQuantity, setClientQuantity] = useState(1);
  
  const updateQuantityAction = newQuantity => {
    setClientQuantity(newQuantity);

    // Access the pending state of the transition,
    // by wrapping in startTransition again.
    startTransition(async () => {
      const savedQuantity = await updateQuantity(newQuantity);
      startTransition(() => {
        setQuantity(savedQuantity);
      });
    });
  };

  return (
    <div>
      <h1>Checkout</h1>
      <Item action={updateQuantityAction}/>
      <hr />
      <Total clientQuantity={clientQuantity} savedQuantity={quantity} isPending={isPending} />
    </div>
  );
}

多次点击时,之前的请求可能在后面的请求完成之后才完成。发生这种情况时,React 目前无法知道预期的顺序。这是因为更新是异步安排的,React 跨异步边界丢失了顺序上下文。

这是预期的结果,因为转换中的操作不保证执行顺序。对于常见用例,React 提供了更高级别的抽象,例如 useActionState<form> 操作,它们可以为您处理排序。对于高级用例,您需要实现自己的排队和中止逻辑来处理这个问题。