useTransition 是一个 React Hook,允许你在后台渲染 UI 的一部分。
const [isPending, startTransition] = useTransition()参考
useTransition()
在组件顶层调用 useTransition,以将某些状态更新标记为 Transition。
import { useTransition } from 'react';
function TabContainer() {
const [isPending, startTransition] = useTransition();
// ...
}参数
useTransition 不接受任何参数。
返回值
useTransition 返回一个包含恰好两个项目的数组:
isPending标志,告诉你是否存在一个待处理的 Transition。startTransition函数,让你可以将更新标记为 Transition。
startTransition(action)
useTransition 返回的 startTransition 函数允许你将更新标记为 Transition。
function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('about');
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
// ...
}参数
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 返回一个包含恰好两个项目的数组:
isPending标志,告诉你是否存在一个待处理的 Transition。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 提供即时反馈。
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> ); }
显示 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> ); }
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 的内容。
构建一个支持 Suspense 的路由器
如果你正在构建一个 React 框架或路由器,我们建议将页面导航标记为 Transitions。
function Router() {
const [page, setPage] = useState('/');
const [isPending, startTransition] = useTransition();
function navigate(url) {
startTransition(() => {
setPage(url);
});
}
// ...这样做有三个原因:
- Transitions 是可中断的,这让用户无需等待重新渲染完成就能切换出去。
- Transitions 可防止不必要的加载指示器,这让用户在导航时避免突兀的跳转。
- Transitions 会等待所有待处理的 actions,这让用户在新页面显示前等待副作用完成。
下面是一个使用 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>; }
使用错误边界向用户显示错误
如果传递给 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,有两种选择:
- 你可以声明两个独立的状态变量:一个用于输入框状态(始终同步更新),另一个用于在 Transition 中更新的状态。这样你就可以使用同步状态来控制输入框,并将 Transition 状态变量(它会“落后于”输入)传递给其余渲染逻辑。
- 另外,你也可以只使用一个状态变量,并添加
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> ); }