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

Note

startTransition 中调用的函数被称为“Actions”。

传递给 startTransition 的函数被称为 “Action”。按照约定,在 startTransition 内部调用的任何回调(例如回调 prop)都应命名为 action,或包含 “Action” 后缀:

function SubmitButton({ submitAction }) {
const [isPending, startTransition] = useTransition();

return (
<button
disabled={isPending}
onClick={() => {
startTransition(async () => {
await submitAction();
});
}}
>
Submit
</button>
);
}

参数

  • action:一个通过调用一个或多个 set 函数 来更新某些状态的函数。React 会立即以无参数的形式调用 action,并将 action 函数调用期间同步调度的所有状态更新标记为 Transitions。action 中任何被 await 的异步调用也会包含在 Transition 中,但目前需要将 await 之后的任何 set 函数再用一个额外的 startTransition 包裹起来(参见故障排除)。被标记为 Transitions 的状态更新将是非阻塞的,并且不会显示不必要的加载指示器

返回值

startTransition 不返回任何内容。

注意事项

  • useTransition 是一个 Hook,因此它只能在组件或自定义 Hook 内部调用。如果你需要在别处启动 Transition(例如从数据库中),请改用独立的 startTransition

  • 只有在你能够访问该状态的 set 函数时,才能将更新包装为 Transition。如果你想根据某个 prop 或自定义 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. isPending 标志,告诉你是否存在一个待处理的 Transition。
  2. startTransition 函数,让你创建一个 Action。

要启动一个 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 会保持响应。例如,如果用户点击了一个标签页,然后又改变主意点击了另一个标签页,那么第二次点击会被立即处理,而无需等待第一次更新完成。

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

Actions 与常规事件处理的区别

Example 1 of 2:
在 Action 中更新数量

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

快速多次更新数量。注意,在任何请求进行中时,pending 的 “Total” 状态会显示出来,而 “Total” 只会在最后一次请求完成后更新。由于更新是在 Action 中进行的,因此在请求进行期间,“quantity” 仍然可以继续更新。

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 => {
    // 要访问 Transition 的 pending 状态,
    // 再次调用 startTransition。
    startTransition(async () => {
      const savedQuantity = await updateQuantity(newQuantity);
      startTransition(() => {
        setQuantity(savedQuantity);
      });
    });
  };

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

这是一个演示 Actions 如何工作的基础示例,但它没有处理请求乱序完成的问题。当多次更新数量时,先前的请求有可能在后续请求之后完成,从而导致数量更新顺序错乱。这是一个已知限制,我们会在未来修复(见下方故障排除)。

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

这些解决方案会帮你处理请求顺序问题。使用 Transitions 构建自己管理异步状态转换的自定义 Hook 或库时,你拥有更高的请求顺序控制能力,但需要自行处理。


从组件中暴露 action prop

你可以从组件中暴露一个 action prop,让父组件可以调用一个 Action。

例如,这个 TabButton 组件将其 onClick 逻辑封装在一个 action prop 中:

export default function TabButton({ action, children, isActive }) {
const [isPending, startTransition] = useTransition();
if (isActive) {
return <b>{children}</b>
}
return (
<button onClick={() => {
startTransition(async () => {
// 等待传入的 action。
// 这样它既可以是同步的,也可以是异步的。
await action();
});
}}>
{children}
</button>
);
}

由于父组件在 action 中更新其状态,该状态更新会被标记为 Transition。这意味着你可以点击 “Posts”,然后立刻点击 “Contact”,而不会阻塞用户交互:

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={async () => {
      startTransition(async () => {
        // 等待传入的 action。
        // 这样它既可以是同步的,也可以是异步的。
        await action();
      });
    }}>
      {children}
    </button>
  );
}

Note

当从组件中暴露一个 action prop 时,你应该在 transition 内部 await 它。

这样 action 回调既可以是同步的,也可以是异步的,而不需要在 action 内部再用额外的 startTransition 去包裹 await


显示 pending 的视觉状态

你可以使用 useTransition 返回的 isPending 布尔值来向用户指示一个 Transition 正在进行中。例如,标签按钮可以有一个特殊的 “pending” 视觉状态:

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

注意,现在点击 “Posts” 感觉更灵敏了,因为标签按钮本身会立即更新:

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(async () => {
        await action();
      });
    }}>
      {children}
    </button>
  );
}


防止不必要的加载指示器

在这个示例中,PostsTab 组件使用 use 获取一些数据。当你点击 “Posts” 标签时,PostsTab 组件会挂起,导致最近的加载 fallback 出现:

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>🌀 正在加载...</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>
  );
}

隐藏整个标签容器来显示加载指示器会让用户体验很突兀。如果你给 TabButton 添加 useTransition,就可以改为在标签按钮中显示 pending 状态。

注意,点击 “Posts” 不再会用一个 spinner 替换整个标签容器:

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(async () => {
        await action();
      });
    }}>
      {children}
    </button>
  );
}

阅读更多关于在 Suspense 中使用 Transitions 的内容。

Note

Transitions 只会“等待”足够长的时间,以避免隐藏已经揭示的内容(比如标签容器)。如果 Posts 标签有一个嵌套的 <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>🌀 正在加载...</h2>;
}

Note

支持 Suspense 的路由器预期会默认将导航更新包装为 Transitions。


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

如果传递给 startTransition 的函数抛出错误,你可以使用 error boundary 向用户显示错误。要使用 error boundary,请将调用 useTransition 的组件包裹在 error boundary 中。一旦传递给 startTransition 的函数出错,就会显示 error boundary 的 fallback。

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

export function AddCommentContainer() {
  return (
    <ErrorBoundary fallback={<p>⚠️ 出了点问题</p>}>
      <AddCommentButton />
    </ErrorBoundary>
  );
}

function addComment(comment) {
  // 为了演示以显示 Error Boundary
  if (comment == null) {
    throw new Error("示例错误:抛出一个错误以触发 error boundary");
  }
}

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

  return (
    <button
      disabled={pending}
      onClick={() => {
        startTransition(() => {
          // 故意不传入 comment
          // 这样就会抛出错误
          addComment();
        });
      }}
    >
      添加评论
    </button>
  );
}


故障排查

在 Transition 中更新输入框不起作用

你不能对控制输入框的状态变量使用 Transition:

const [text, setText] = useState('');
// ...
function handleChange(e) {
// ❌ 不能将 Transitions 用于受控输入的状态
startTransition(() => {
setText(e.target.value);
});
}
// ...
return <input value={text} onChange={handleChange} />;

这是因为 Transitions 是非阻塞的,但响应 change 事件来更新输入框应该同步发生。如果你想在输入时运行 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);

React 没有把 await 之后的状态更新视为 Transition

当你在 startTransition 函数内部使用 await 时,发生在 await 之后的状态更新不会被标记为 Transitions。你必须在每个 await 之后使用 startTransition 调用来包裹状态更新:

startTransition(async () => {
await someAsyncFunction();
// ❌ 在 await 之后没有使用 startTransition
setPage('/about');
});

不过,下面这样才可以:

startTransition(async () => {
await someAsyncFunction();
// ✅ 在 await *之后* 使用 startTransition
startTransition(() => {
setPage('/about');
});
});

这是一个 JavaScript 限制,因为 React 会丢失 async 上下文的作用域。未来当 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 会立即执行你的函数,但任何在其运行期间调度的状态更新都会被标记为 Transitions。你可以把它想象成这样:

// React 工作方式的简化版本

let isInsideTransition = false;

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

function setState() {
if (isInsideTransition) {
// ... 调度一个 Transition 状态更新 ...
} else {
// ... 调度一个紧急状态更新 ...
}
}

我在 Transitions 中的状态更新顺序乱了

如果你在 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();
  // 将实际数量存储在单独的状态中,以显示不匹配。
  const [clientQuantity, setClientQuantity] = useState(1);

  const updateQuantityAction = newQuantity => {
    setClientQuantity(newQuantity);

    // 通过再次包裹在 startTransition 中,
    // 访问该 Transition 的 pending 状态。
    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 会在异步边界之间丢失顺序上下文。

这是预期行为,因为 Transition 中的 Action 不保证执行顺序。对于常见用例,React 提供了更高层的抽象,例如 useActionState<form> actions,它们会帮你处理顺序。对于高级用例,你需要自己实现排队和中止逻辑来处理这一点。

useActionState 处理执行顺序的示例:

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

export default function App({}) {
  // 将实际数量存储在单独的状态中,以显示不匹配。
  const [clientQuantity, setClientQuantity] = useState(1);
  const [quantity, updateQuantityAction, isPending] = useActionState(
    async (prevState, payload) => {
      setClientQuantity(payload);
      const savedQuantity = await updateQuantity(payload);
      return savedQuantity; // 返回新数量以更新状态
    },
    1 // 初始数量
  );

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