useTransition 让你把一次状态更新标记为非紧急。React 会在后台渲染这次更新,如果有更紧急的事情发生,比如一次点击或一次按键,它就丢弃或重新开始这次渲染。这样,在屏幕上较慢的部分追上来之前,页面始终能及时响应。
先点 posts,然后在列表出现之前点 contact。contact 会立刻显示:React 把渲染到一半的 posts 丢掉了。现在把 startTransition(() => { setTab(next); }) 改成普通的 setTab(next);,再做一遍同样的操作:点 posts 之后页面会卡住大约 300 ms,你对 contact 的点击要等 posts 渲染完才会生效。
语法
const [isPending, startTransition] = useTransition();
startTransition(() => {
setTab(next);
});
startTransition接收一个函数并立即执行它。函数里的所有状态更新都会被标记为 transition。isPending从你开始 transition 的那一刻起为true,直到 React 渲染完成并提交结果。
useTransition 不接收任何参数。你传入的函数是同步执行的;transition 影响的是 React 如何渲染由此产生的更新,而不是你的函数什么时候运行。
“非紧急”是什么意思
React 把更新分成两类。紧急更新,比如输入、点击和按下按键,必须立刻反映出来,否则页面会让人觉得坏了。transition 更新,比如切换到新视图或显示筛选结果,可以稍等片刻。
React 会把 transition 拆成小块来渲染,并在组件之间检查有没有紧急的事情进来。如果有,React 会暂停或放弃这次 transition,先处理紧急更新,再用最新的状态重新开始 transition。transition 渲染期间,之前的画面保持不变,React 不会显示一个做到一半的结果。所以在上面的示例里,posts 准备好之前,页面一直显示 about 的文字,按钮旁边显示 Loading 提示。
中断只发生在组件之间。一个阻塞 300 ms 的组件无法被切成两半,这就是为什么上面把耗时的工作分散到了 300 个小的 SlowPost 组件里。
显示等待提示
isPending 适合用来显示不替换当前内容的加载指示:按钮旁的加载图标、变暗的文字、一个“Saving...”标签。在第一个示例里,它在标签旁边显示 Loading...。你也可以用它来设置内容的样式:
<div style={{ opacity: isPending ? 0.6 : 1 }}>{content}</div>
输入时筛选一个大列表
你不能把输入框自己的值放进 transition。受控的 <input> 必须在同一次紧急更新里拿到新值,否则会丢字符。正确的做法是维护两份状态:输入框的文字(紧急)和列表使用的查询词(transition)。
快速输入 123。输入框一点都不卡。控制台仍然会在每次按键时打印一行 rendering results,因为 Results 在 React 开始渲染它时就会打印日志,但 1 和 12 对应的列表从未出现在屏幕上:每一次新的按键都打断了正在进行的渲染,React 用最新的查询词重新开始。Results 特意用 memo 包了起来:如果不这样做,每次按键的紧急渲染也会用旧的查询词渲染那个很慢的列表,transition 带来的好处就没有了。
不用 Hook 的 startTransition
react 还把 startTransition 作为普通函数导出。它和 Hook 一样把更新标记为 transition,但你拿不到 isPending。在不能调用 Hook 的地方使用它,比如组件之外的数据请求库或路由库里。
import { startTransition } from 'react';
function navigate(url) {
startTransition(() => {
setCurrentUrl(url);
});
}
React 19 中的异步 transition
从 React 19 开始,你传入的函数可以是 async 的。这时 isPending 会在函数运行的整个过程中保持 true,包括等待的时间,因此用它在请求期间显示等待状态非常简单。下面这个模拟的 saveName 代替了真实的请求。
注意 setSaved 外面的第二个 startTransition。跨过 await 之后,React 就跟丢了这次 transition,所以它之后的状态更新要再包一次,才算作 transition 的一部分。以这种方式传入的函数,React 称之为 action;useActionState 和表单 action 都建立在同样的机制之上。
transition 不是防抖
防抖会在最后一次按键之后等待一段固定时间,再开始做事。transition 不会等待。它立刻开始渲染,如果有新的更新到来,就丢掉旧的工作。在速度快的设备上,结果完全没有延迟;在速度慢的设备上,React 会在两次按键之间尽可能多地完成工作。transition 做不到的是减少网络请求的次数:如果每次按键都会发起一次 fetch,请对 fetch 做防抖。
transition 与 Suspense
transition 还会改变 Suspense 边界的行为。通常,当边界内的某个组件挂起(比如懒加载组件正在加载,或者用 use 读取数据)时,React 会把内容替换成 fallback。如果导致挂起的那次更新是 transition,React 则会保留屏幕上已有的内容,isPending 一直为 true,直到新内容准备就绪。路由库正是这样避免每次导航都闪出一个整页的加载图标:它们把导航包在 transition 里,旧页面一直可见,直到下一个页面能够渲染。从未显示过的内容仍然会使用 fallback,所以新出现的边界照常工作。
不起作用的情况
- 在定时器里更新。
startTransition(() => setTimeout(() => setTab(next), 100))不会标记这次更新,因为它在startTransition返回之后才运行。应该把startTransition放到定时器里面。 - 单个很慢的组件。 React 只能在组件之间中断,所以一个长时间阻塞的组件依然会阻塞。
- 控制输入框。 如前所述,输入框自己的状态要保持紧急更新。
如果状态的 setter 不归你管,你只能拿到一个值(比如 prop),useDeferredValue 能给你同样的效果。
常见问题
useTransition 是做什么的?
它给你两样东西:startTransition 把其中的状态更新标记为非紧急;isPending 在这次更新还在渲染时为 true。React 可以中断非紧急的渲染,优先处理点击和输入。
useTransition 和 startTransition 有什么区别?
两者都把更新标记为 transition。useTransition 这个 Hook 还会给你 isPending,用来显示加载提示。从 react 导入的独立 startTransition 可以在组件外使用,但没有 pending 标志。
useTransition 能用在文本输入框上吗?
不能用在输入框自己的值上。受控输入框必须同步更新,否则输入会出问题。让输入框的状态保持紧急更新,把依赖它的耗时更新放进 transition,或者改用 useDeferredValue。
useTransition 和防抖一样吗?
不一样。防抖会先等一段固定的时间再做事。transition 会立刻开始渲染,有新的更新到来时就被中断,所以在速度快的设备上,结果几乎没有延迟就会出现。
startTransition 可以接收 async 函数吗?
可以,从 React 19 开始支持。isPending 会一直保持 true,直到这个 async 函数执行完毕。await 之后的状态更新必须再包进一次 startTransition 调用,才会被标记为 transition。