useTransition
是一个 React Hook,它允许你在不阻塞 UI 的情况下更新状态。
const [isPending, startTransition] = useTransition()
参考
useTransition()
在组件的顶层调用 useTransition
来将某些状态更新标记为过渡。
import { useTransition } from 'react';
function TabContainer() {
const [isPending, startTransition] = useTransition();
// ...
}
参数
useTransition
不接受任何参数。
返回值
useTransition
返回一个包含两个项目的数组
isPending
标志,告诉你是否有一个待处理的过渡。startTransition
函数,它允许你将状态更新标记为过渡。
startTransition
函数
由 useTransition
返回的 startTransition
函数允许您将状态更新标记为 Transition(过渡)。
function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('about');
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
// ...
}
参数
scope
:一个通过调用一个或多个set
函数 来更新某些状态的函数。React 会立即调用不带参数的scope
,并将scope
函数调用期间同步安排的所有状态更新标记为 Transitions。它们将是非阻塞的,并且 不会显示不必要的加载指示器。
返回值
startTransition
不返回任何内容。
注意事项
-
useTransition
是一个 Hook,因此只能在组件或自定义 Hook 内部调用。如果您需要在其他地方(例如,从数据库)启动 Transition,请调用独立的startTransition
。 -
只有在您可以访问该状态的
set
函数的情况下,才能将更新包装到 Transition 中。如果您想响应某些 prop 或自定义 Hook 值来启动 Transition,请尝试使用useDeferredValue
。 -
您传递给
startTransition
的函数必须是同步的。React 会立即执行此函数,并将执行期间发生的所有状态更新标记为 Transitions。如果您尝试稍后执行更多状态更新(例如,在超时中),则它们不会被标记为 Transitions。 -
标记为 Transition 的状态更新将被其他状态更新中断。例如,如果您在 Transition 中更新了图表组件,但在图表处于重新渲染过程中开始在输入框中键入内容,则 React 会在处理输入更新后重新启动图表组件上的渲染工作。
-
Transition 更新不能用于控制文本输入。
-
如果有多个正在进行的 Transitions,React 目前会将它们一起批处理。这是一个限制,可能会在未来的版本中删除。
用法
将状态更新标记为非阻塞 Transition
在组件的顶层调用 useTransition
,以将状态更新标记为非阻塞的*Transitions*。
import { useState, useTransition } from 'react';
function TabContainer() {
const [isPending, startTransition] = useTransition();
// ...
}
useTransition
返回一个包含两个项目的数组
isPending
标志 告诉您是否存在挂起的 Transition。startTransition
函数 允许您将状态更新标记为 Transition。
然后,您可以像这样将状态更新标记为 Transition:
function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('about');
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
// ...
}
Transitions 允许您即使在速度较慢的设备上也能保持用户界面更新的响应速度。
使用 Transition,您的 UI 在重新渲染过程中保持响应。例如,如果用户单击了一个选项卡,但随后改变主意并单击了另一个选项卡,则他们无需等待第一次重新渲染完成即可执行此操作。
示例: 1关于 2: 在 Transition 中更新当前选项卡
在此示例中,“帖子”选项卡被**人为地放慢了**,因此渲染至少需要一秒钟。
单击“帖子”,然后立即单击“联系我们”。请注意,这会中断“帖子”的缓慢渲染。“联系我们”选项卡会立即显示。因为此状态更新被标记为 Transition,所以缓慢的重新渲染不会冻结用户界面。
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 中更新父组件
您也可以从 useTransition
调用更新父组件的状态。例如,这个 TabButton
组件将其 onClick
逻辑包装在一个 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
事件处理程序中更新其状态,所以该状态更新被标记为 Transition。这就是为什么,就像在前面的例子中一样,您可以点击“帖子”,然后立即点击“联系”。更新所选选项卡被标记为 Transition,因此它不会阻塞用户交互。
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> ); }
在 Transition 期间显示挂起视觉状态
您可以使用 useTransition
返回的 isPending
布尔值来向用户指示 Transition 正在进行中。例如,选项卡按钮可以有一个特殊的“挂起”视觉状态
function TabButton({ children, isActive, onClick }) {
const [isPending, startTransition] = useTransition();
// ...
if (isPending) {
return <b className="pending">{children}</b>;
}
// ...
请注意,点击“帖子”现在感觉响应更快,因为选项卡按钮本身会立即更新
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> ); }
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
中,则可以改为在选项卡按钮中指示显示挂起状态。
请注意,点击“帖子”不再会用微调器替换整个选项卡容器
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> ); }
阅读有关将 Transitions 与 Suspense 一起使用的更多信息。
构建支持 Suspense 的路由器
如果您正在构建 React 框架或路由器,我们建议将页面导航标记为 Transitions。
function Router() {
const [page, setPage] = useState('/');
const [isPending, startTransition] = useTransition();
function navigate(url) {
startTransition(() => {
setPage(url);
});
}
// ...
建议这样做有两个原因
- Transitions 是可中断的,这允许用户在不等待重新渲染完成的情况下点击离开。
- Transitions 可以防止不必要的加载指示器,这可以让用户避免在导航时出现 jarring jumps。
下面是一个使用 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>🌀 Loading...</h2>; }
使用错误边界向用户显示错误
如果传递给 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> ); }
故障排除
在 Transition 中更新输入框无效
您不能将 Transition 用于控制输入框的状态变量
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 是非阻塞的,但响应更改事件更新输入框应该是同步发生的。如果您想在输入时运行 Transition,您有两个选择
- 您可以声明两个独立的状态变量:一个用于输入框状态(始终同步更新),一个将在 Transition 中更新。这使您可以使用同步状态控制输入框,并将 Transition 状态变量(将“滞后于”输入框)传递给其余的渲染逻辑。
- 或者,您可以使用一个状态变量,并添加
useDeferredValue
,它将“滞后于”实际值。它将触发非阻塞的重新渲染,以自动“赶上”新值。
React 没有将我的状态更新视为 Transition
当您将状态更新包裹在 Transition 中时,请确保它发生在 startTransition
调用期间
startTransition(() => {
// ✅ Setting state *during* startTransition call
setPage('/about');
});
您传递给 startTransition
的函数必须是同步的。
您不能像这样将更新标记为 Transition
startTransition(() => {
// ❌ Setting state *after* startTransition call
setTimeout(() => {
setPage('/about');
}, 1000);
});
相反,您可以这样做
setTimeout(() => {
startTransition(() => {
// ✅ Setting state *during* startTransition call
setPage('/about');
});
}, 1000);
类似地,您不能像这样将更新标记为 Transition
startTransition(async () => {
await someAsyncFunction();
// ❌ Setting state *after* startTransition call
setPage('/about');
});
但是,这样做是可行的
await someAsyncFunction();
startTransition(() => {
// ✅ Setting state *during* startTransition call
setPage('/about');
});
我想从组件外部调用 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。您可以想象它是这样工作的
// 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 ...
}
}