React useTransition
问题的起点:为什么需要 transition
在 React 18 之前,所有的状态更新都是「紧急」的。当用户点击一个按钮,React 立即开始处理这个点击事件,更新状态,重新渲染组件。这个过程是不可中断的——如果一次状态更新涉及大量计算,浏览器的主线程会被阻塞,用户会感受到界面卡顿。
考虑一个常见的场景:用户在搜索框输入关键词,后端返回匹配结果。如果搜索结果非常庞大(比如 thousands of items),渲染这个列表可能需要几百毫秒。在这几百毫秒内,用户的输入响应(光标移动、字符显示)会被延迟,因为浏览器主线程被渲染任务占用。
React 18 引入的并发特性(Concurrent Features)解决了这个问题。它的核心思想是:不是所有的状态更新都应该有相同的优先级。用户输入是紧急的,需要立即响应;搜索结果的渲染是「非紧急」的,可以稍微延迟一下,让浏览器有时间处理用户输入。
useTransition 就是 React 提供的、让我们能够标记「哪些更新是紧急的,哪些可以延迟」的 API。
紧急更新 vs 非紧急更新
理解 useTransition,首先要理解 React 对状态更新的分类。
紧急更新(Urgent Updates) 是那些用户期望立即看到反馈的交互,比如:
- 文本输入(每个字符都应该立即显示)
- 按钮点击(按钮应该在毫秒级别响应)
- 拖拽操作(位置变化应该实时跟随鼠标)
非紧急更新(Non-Urgent Updates / Transition Updates) 是那些「慢一点也没关系」的更新,比如:
- 搜索结果列表的渲染
- 复杂表单的完整校验结果
- 页面切换动画
- 大数据的排序或过滤
用 useTransition 包装的更新会被标记为非紧急的。React 会在处理紧急更新之余,利用空闲时间来处理这些被标记的更新。如果用户又开始输入新内容,React 会中断正在进行的非紧急更新,优先处理紧急输入。
useTransition 的基本 API
useTransition 返回一个数组,包含两个元素:
const [isPending, startTransition] = useTransition();
- isPending:一个布尔值,表示一个 transition 是否正在进行中。当 transition 还没完成时为
true,完成后为false。 - startTransition:一个函数,用于将更新标记为 transition。
基础用法
import { useState, useTransition } from 'react';
function Search({ items }) {
const [query, setQuery] = useState('');
const [filteredItems, setFilteredItems] = useState([]);
const [isPending, startTransition] = useTransition();
function handleInput(e) {
const value = e.target.value;
setQuery(value); // 紧急更新:立即反映用户输入
startTransition(() => {
// 这个闭包里的更新是「非紧急」的
const matched = items.filter(item =>
item.name.toLowerCase().includes(value.toLowerCase())
);
setFilteredItems(matched);
});
}
return (
<div>
<input value={query} onChange={handleInput} />
{isPending ? (
<Spinner />
) : (
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)}
</div>
);
}
在这个例子里:
- 用户输入
setQuery(value)是紧急更新,光标移动和字符显示是即时的 setFilteredItems(matched)在startTransition里,是非紧急更新- 如果用户在列表还在渲染时继续输入,React 会中断列表渲染,优先处理输入
isPending 的作用
isPending 让我们可以在 transition 进行时显示加载状态。这比传统的 isLoading 更精确——传统方案需要额外的状态来跟踪「是不是正在过滤」,而 isPending 直接告诉我们「transition 是否还没完成」。
function SearchResults({ query }) {
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
useEffect(() => {
startTransition(() => {
setResults(performSearch(query));
});
}, [query]);
if (isPending) {
return <Skeleton />;
}
return <SearchResultsList results={results} />;
}
典型场景:搜索输入中的大列表延迟渲染
搜索场景是 useTransition 最典型的应用场景之一。考虑一个「模糊搜索」功能:
function FuzzySearch({ allContacts }) {
const [input, setInput] = useState('');
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
// 模拟防抖:每次 input 变化时,用 transition 更新 query
function handleInput(e) {
setInput(e.target.value);
startTransition(() => {
// 这个更新可能被中断
setQuery(e.target.value);
});
}
// query 变化时执行搜索
const matchedContacts = useMemo(() => {
return allContacts.filter(c =>
c.name.toLowerCase().includes(query.toLowerCase())
);
}, [allContacts, query]);
return (
<div>
<input value={input} onChange={handleInput} placeholder="Search..." />
<div style={{ opacity: isPending ? 0.5 : 1 }}>
{matchedContacts.map(contact => (
<ContactCard key={contact.id} contact={contact} />
))}
</div>
</div>
);
}
这个例子里,即使匹配结果有 thousands of contacts,用户输入也不会感到卡顿,因为:
- 输入本身是紧急更新,立即响应
- 列表的过滤和渲染在 transition 中进行,可以被中断
为什么不用 useEffect + 防抖
传统的防抖方案有根本的缺陷:防抖是基于时间的延迟,不管你的电脑多快,都必须等固定的时间才会执行。而 startTransition 是基于优先级的延迟——React 只会在「没有紧急更新要处理」时才执行非紧急更新。
如果用户的电脑很快,防抖也要等固定时间(体验差);如果用户的电脑很慢,防抖时间设置得再长也可能不够。startTransition 则智能得多:它让 React 自己决定什么时候执行非紧急更新,而不是强制设定一个时间。
startTransition 独立 API
除了 useTransition 返回的 startTransition,React 还提供了一个独立的 startTransition 函数:
import { startTransition } from 'react';
function handleClick() {
startTransition(() => {
setState(newState);
});
}
独立 startTransition 和 useTransition 返回的 startTransition 功能完全相同。区别在于:
useTransition返回的版本同时提供isPending状态- 独立
startTransition更简洁,适合不需要追踪 pending 状态的场景
常见用法
import { useState, startTransition } from 'react';
function ImageGallery({ images }) {
const [selectedId, setSelectedId] = useState(null);
const [filter, setFilter] = useState('all');
function handleFilterChange(newFilter) {
startTransition(() => {
// 滤镜切换可能是昂贵的操作
// 用 transition 标记,允许被紧急更新中断
setFilter(newFilter);
});
}
function handleSelect(id) {
setSelectedId(id); // 立即响应
}
return (
<div>
<FilterBar selected={filter} onChange={handleFilterChange} />
<ImageGrid
images={images}
filter={filter}
selectedId={selectedId}
onSelect={handleSelect}
/>
</div>
);
}
useTransition vs setTimeout:为什么不直接用 setTimeout
初学者可能会有疑问:「既然是要延迟执行,为什么不直接用 setTimeout?」
// 为什么不这样写?
function Search({ items }) {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
function handleInput(e) {
setQuery(e.target.value);
setTimeout(() => {
setResults(filterItems(e.target.value));
}, 100);
}
}
setTimeout 有几个根本性的问题:
问题一:setTimeout 是基于时间的,不感知优先级
setTimeout 只管「等了多少时间」,不管「现在有没有紧急任务」。即使浏览器正在处理紧急的输入事件,setTimeout 时间到了就会执行,可能导致界面卡顿。
useTransition 让 React 自己调度——React 只会在没有紧急任务时才执行 transition。这意味着:
- 在快的机器上,transition 可能很快就执行完
- 在慢的机器上,transition 会等待,但不会阻塞用户输入
问题二:setTimeout 无法被中断
一旦 setTimeout 的回调开始执行,它会完整运行,不会被中断。如果用户在 setTimeout 执行期间又输入了新内容,setTimeout 不会「取消」正在执行的过滤操作。
function handleInput(e) {
const value = e.target.value;
setQuery(value);
setTimeout(() => {
// 假设这个过滤很慢,需要 500ms
const result = expensiveFilter(value);
setResults(result);
}, 100);
}
如果用户在 100ms 后又输入了新内容,setTimeout 的回调仍然会在 500ms 后执行(基于旧的 value),可能覆盖用户已经看到的更新。
useTransition 则会自动处理这种情况:React 会记住「哦,还有一个更新的 transition 要执行」,在适当的时候会丢弃旧的 transition,执行新的。
问题三:setTimeout 无法提供 pending 状态
setTimeout 不知道「正在等待」这个概念。用 setTimeout 实现 pending 状态需要额外的状态管理:
// 用 setTimeout 实现 pending
const [isPending, setIsPending] = useState(false);
function handleInput(e) {
setQuery(e.target.value);
setIsPending(true);
setTimeout(() => {
const result = filterItems(e.target.value);
setResults(result);
setIsPending(false);
}, 100);
}
但这引入了新的问题:如果用户快速连续输入,多个 setTimeout 会叠加,isPending 可能出现不一致的状态。useTransition 的 isPending 是 React 内部管理的,不会有这个问题。
并发渲染下 startTransition 的行为
React 18 的并发渲染器(concurrent renderer)允许它同时处理多个任务,并通过「时间切片」(time slicing)把工作分散到多个帧上。
当 startTransition 标记一个更新为非紧急时,React 的行为是:
- 开始执行 transition:React 开始处理被标记为 transition 的状态更新
- 可中断:如果在这个过程中有紧急更新进来,React 会中断当前的 transition
- 保持旧状态可见:被中断时,用户仍然看到的是 transition 开始前的旧状态(不会看到「半成品」状态)
- 继续或重启:当紧急更新处理完后,React 会恢复或重新开始被中断的 transition
这个机制保证了 UI 的响应性:紧急更新总是优先被处理,用户不会感到界面卡顿。
transition 与 Suspense 的结合
startTransition 在和 Suspense 结合时特别强大。考虑这个场景:
function Router() {
const [page, setPage] = useState('home');
return (
<Suspense fallback={<PageSkeleton />}>
{page === 'home' && <HomePage />}
{page === 'about' && <AboutPage />}
{page === 'contact' && <ContactPage />}
</Suspense>
);
}
如果 HomePage 加载很慢,传统方案会导致整个页面出现 loading 态。但如果用 startTransition:
function handleNavigation(newPage) {
startTransition(() => {
setPage(newPage);
});
}
此时 Suspense 的 fallback 不会立即触发——React 会等待一段时间,看看 transition 能不能很快完成。只有当 transition 时间太长时,Suspense 才会显示 fallback。这创造了更流畅的导航体验。
什么情况下会打断 transition
不是所有的状态更新都会打断 transition。以下情况不会打断正在进行的 transition:
- 其他 transition:用
startTransition标记的更新会排队等候 - useState 的函数式更新(如果依赖正确):如果你用
setState(prev => ...)且依赖正确,不会打断
以下情况会打断 transition:
- 非 transition 的紧急状态更新:直接调用
setState(不是startTransition)会立即打断所有正在进行的 transition - 用户输入事件:
onChange、onClick等会立即打断 transition - setTimeout 内的更新:如果在 transition 进行中有一个 setTimeout 触发了
setState,这个setState是紧急的,会打断 transition
function Component() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
function handleChange(e) {
const value = e.target.value;
setQuery(value); // 紧急更新,会打断任何正在进行的 transition
startTransition(() => {
setResults(filterItems(value));
});
}
}
这个例子里,如果用户在 transition 进行中继续输入,setQuery(value) 是紧急更新,会打断列表渲染的 transition,但不会打断 setResults 的 transition(因为 setResults 在同一个 startTransition 里)。
useTransition 的限制和注意事项
1. 只能在 React 可以追踪的地方使用
startTransition 不是魔法。它只能在 React 的调度系统内工作。如果你的更新在 React 无法追踪的地方(比如普通的 JavaScript 对象操作、第三方库的状态),startTransition 无能为力。
2. transition 内的更新不保证原子性
一个 transition 内的多个 setState 可能被分开执行。如果你在 transition 内连续调用三次 setState,React 可能只执行第一个,后面的被中断或合并。
startTransition(() => {
setA(valueA); // 可能执行
setB(valueB); // 可能被跳过
setC(valueC); // 可能被跳过
});
这不是 bug,是 React 调度机制的设计。如果需要保证原子性,考虑用 useReducer 把多个状态合并成一个对象。
3. 过度使用 transition 可能导致 UI 看起来「慢」
如果到处都是 transition,用户可能会觉得「为什么点了没反应」。transition 应该是选择性的——只用于确实有性能问题的场景。
4. 旧浏览器不支持
React 18 的 concurrent features 包括 useTransition。如果需要支持 React 18 之前的版本,这个 API 不可用。
面试中的表达
面试官问 useTransition,通常想确认你理解 React 18 的并发模型和「紧急 vs 非紧急更新」的概念:
useTransition 是 React 18 引入的 API,用来标记某些状态更新是「非紧急」的。在 React 18 之前,所有状态更新都是紧急的,浏览器主线程被占用时 UI 就会卡顿。useTransition 让我们可以区分「用户输入」这种紧急更新和「搜索结果渲染」这种非紧急更新。
useTransition 返回的 isPending 可以在 transition 进行时显示加载态,这比传统的防抖加 loading 状态更精确。useTransition 之所以比 setTimeout 好,不是因为它更快,而是因为它是基于优先级的调度——React 只在没有紧急任务时才执行 transition,而不是强制等待一个固定时间。
另一个重点是 transition 的可中断性:如果用户在 transition 进行中又开始输入,React 会立即处理输入(紧急),然后在空闲时恢复或重新开始 transition。这保证了 UI 始终保持响应。
延展阅读
- React Docs: useTransition — 官方 useTransition 文档
- React Beta Docs: Concurrent UI Patterns — React 18 并发特性介绍博文中关于 transition 的部分
- Dan Abramov: Inside React — 搜索 "transition" 相关文章,深入理解 React 调度机制
- React 18: Working with Suspense — Suspense 与 transition 的配合