useTransition
是一个让你可以在后台渲染部分 UI 的 React Hook。
const [isPending, startTransition] = useTransition()
参考
useTransition()
在组件顶层调用 useTransition
,将某些状态更新标记为 transition。
import { useTransition } from 'react';
function TabContainer() {
const [isPending, startTransition] = useTransition();
// ……
}
参数
useTransition
不需要任何参数。
返回值
useTransition
返回一个由两个元素组成的数组:
isPending
,告诉你是否存在待处理的 transition。startTransition
函数,你可以使用此方法将更新标记为 transition。
startTransition
函数
useTransition
返回的 startTransition
函数允许你将更新标记为 Transition。
function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('about');
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
// ……
}
参数
action
: A function that updates some state by calling one or moreset
functions. React callsaction
immediately with no parameters and marks all state updates scheduled synchronously during theaction
function call as Transitions. Any async calls that are awaited in theaction
will be included in the Transition, but currently require wrapping anyset
functions after theawait
in an additionalstartTransition
(see Troubleshooting). State updates marked as Transitions will be non-blocking and will not display unwanted loading indicators.
返回值
startTransition
不返回任何值。
注意
-
useTransition
是一个 Hook,因此只能在组件或自定义 Hook 内部调用。如果需要在其他地方启动 transition(例如从数据库),请调用独立的startTransition
函数。 -
只有在可以访问该状态的
set
函数时,才能将其对应的状态更新包装为 transition。如果你想启用 Transition 以响应某个 prop 或自定义 Hook 值,请尝试使用useDeferredValue
。 -
传递给
startTransition
的函数会被立即执行,并将在其执行期间发生的所有状态更新标记为 transition。如果你尝试在setTimeout
中执行状态更新,它们将不会被标记为 transition。 -
You must wrap any state updates after any async requests in another
startTransition
to mark them as Transitions. This is a known limitation that we will fix in the future (see Troubleshooting). -
startTransition
函数具有稳定的标识,所以你经常会看到 Effect 的依赖数组中会省略它,即使包含它也不会导致 Effect 重新触发。如果 linter 允许你省略依赖项并且没有报错,那么你就可以安全地省略它。了解移除 Effect 依赖项的更多信息。 -
标记为 Transition 的状态更新将被其他状态更新打断。例如在 Transition 中更新图表组件,并在图表组件仍在重新渲染时继续在输入框中输入,React 将首先处理输入框的更新,之后再重新启动对图表组件的渲染工作。
-
Transition 更新不能用于控制文本输入。
-
目前,React 会批处理多个同时进行的 transition。这是一个限制,可能会在未来版本中删除。
用法
通过 Action 执行非阻塞更新
在组件的顶层调用 useTransition
来创建 Action,并获取挂起的状态:
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);
});
});
}
// ……
}
The function passed to startTransition
is called the “Action”. You can update state and (optionally) perform side effects within an Action, and the work will be done in the background without blocking user interactions on the page. A Transition can include multiple Actions, and while a Transition is in progress, your UI stays responsive. For example, if the user clicks a tab but then changes their mind and clicks another tab, the second click will be immediately handled without waiting for the first update to finish.
To give the user feedback about in-progress Transitions, to isPending
state switches to true
at the first call to startTransition
, and stays true
until all Actions complete and the final state is shown to the user. Transitions ensure side effects in Actions to complete in order to prevent unwanted loading indicators, and you can provide immediate feedback while the Transition is in progress with useOptimistic
.
第 1 个示例 共 2 个挑战: Updating the quantity in an Action
In this example, the updateQuantity
function simulates a request to the server to update the item’s quantity in the cart. This function is artificially slowed down so that it takes at least a second to complete the request.
Update the quantity multiple times quickly. Notice that the pending “Total” state is shown while any requests are in progress, and the “Total” updates only after the final request is complete. Because the update is in an Action, the “quantity” can continue to be updated while the request is in progress.
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> ); }
This is a basic example to demonstrate how Actions work, but this example does not handle requests completing out of order. When updating the quantity multiple times, it’s possible for the previous requests to finish after later requests causing the quantity to update out of order. This is a known limitation that we will fix in the future (see Troubleshooting below).
For common use cases, React provides built-in abstractions such as:
These solutions handle request ordering for you. When using Transitions to build your own custom hooks or libraries that manage async state transitions, you have greater control over the request ordering, but you must handle it yourself.
在组件中公开 action
属性
You can expose an action
prop from a component to allow a parent to call an Action.
For example, this TabButton
component wraps its onClick
logic in an action
prop:
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。这意味着你可以在点击“Posts”后立即点击“Contact”,并且它不会阻止用户交互:
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 中。例如,选项卡按钮可以有一个特殊的“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(() => { 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>🌀 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> ); }
构建一个Suspense-enabled 的路由
如果你正在构建一个 React 框架或路由,我们建议将页面导航标记为转换效果。
function Router() {
const [page, setPage] = useState('/');
const [isPending, startTransition] = useTransition();
function navigate(url) {
startTransition(() => {
setPage(url);
});
}
// ...
这么做有三个好处:
- 转换效果是可中断的,这样用户可以在等待重新渲染完成之前点击其他地方。
- 转换效果可以防止不必要的加载指示符,这样用户就可以避免在导航时产生不协调的跳转。
- Transition 等待所有挂起的 action,它允许用户在副作用完成之后再显示新页面。
下面是一个简单的使用转换效果进行页面导航的路由器示例:
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>; }
Displaying an error to users with an error boundary
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,那么有两种做法:
- 声明两个独立的状态变量:一个用于输入状态(它总是同步更新),另一个用于在 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
When you use await
inside a startTransition
function, the state updates that happen after the await
are not marked as Transitions. You must wrap state updates after each await
in a startTransition
call:
startTransition(async () => {
await someAsyncFunction();
// ❌ 不要在 await 之后调用 startTransition
setPage('/about');
});
然而,使用以下方法可以正常工作:
startTransition(async () => {
await someAsyncFunction();
// ✅ 在 startTransition **之后** 再 await
startTransition(() => {
setPage('/about');
});
});
This is a JavaScript limitation due to React losing the scope of the async context. In the future, when AsyncContext is available, this limitation will be removed.
我想在组件外部调用 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 {
// ……安排紧急状态更新……
}
}
My state updates in Transitions are out of order
If you await
inside startTransition
, you might see the updates happen out of order.
In this example, the updateQuantity
function simulates a request to the server to update the item’s quantity in the cart. This function artificially returns the every other request after the previous to simulate race conditions for network requests.
Try updating the quantity once, then update it quickly multiple times. You might see the incorrect total:
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> ); }
When clicking multiple times, it’s possible for previous requests to finish after later requests. When this happens, React currently has no way to know the intended order. This is because the updates are scheduled asynchronously, and React loses context of the order across the async boundary.
This is expected, because Actions within a Transition do not guarantee execution order. For common use cases, React provides higher-level abstractions like useActionState
and <form>
actions that handle ordering for you. For advanced use cases, you’ll need to implement your own queuing and abort logic to handle this.