一句话定义
两个面向用户的并发 API:useTransition 把某次更新标记为非紧急(并告诉你是否进行中),useDeferredValue 把某个值的下游渲染标记为非紧急(值本身立即更新)——一个控制「动作」,一个控制「值」。
为什么重要
它们是并发渲染从理论到手感的最短路径:两三行代码就能消除「输入卡顿」和「fallback 闪烁」两类高频体验问题,也是 KP-022 概念的最佳实验场。
前置知识
核心概念
- isPending:useTransition 返回的布尔——过渡渲染尚未提交时为 true,用于显示「旧的还在用,新的在路上」。
- startTransition(fn):fn 内的 setState 都以过渡优先级排队。
- useDeferredValue(value):返回「滞后一拍」的版本:value 变化时先用旧值渲染(快),再用新值重渲染(过渡优先级)。
- 与 debounce 的本质区别:debounce 固定延迟、丢中间帧;deferred 不丢帧、不固定延迟——只是让慢的渲染让路。
- 适用判据:更新结果可以晚一点出现(列表过滤、图表、搜索结果);输入框回显这类「必须立即」的绝不放 transition 里。
原理 / 机制
useTransition 完整形状:
jsxfunction FilterableList({ items }) {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
function onChange(e) {
setQuery(e.target.value); // 输入框:立即
startTransition(() => {
setAppliedQuery(e.target.value); // 列表:过渡(可让路、可重放)
});
}
return (
<>
<input value={query} onChange={onChange} />
<div className={isPending ? 'list--updating' : ''}>
<List items={filter(items, appliedQuery)} />
</div>
</>
);
}
useDeferredValue 的等价改写(更少样板,适合「值驱动」的场景):
jsxfunction FilterableList({ items, query }) {
const deferredQuery = useDeferredValue(query); // query 立即用,deferredQuery 滞后
return <List items={filter(items, deferredQuery)} />;
// 父组件用 query 传给输入框,用 deferredQuery 传给重计算部分
}
两个 API 的选择:
| useTransition | useDeferredValue | |
|---|---|---|
| 控制对象 | 一组 setState 调用 | 一个值的下游渲染 |
| 能显示进行中 | ✅ isPending | ❌(可自行比对两值差异) |
| 代码量 | 多(拆两个 state) | 少(一行) |
| 典型场景 | 分页/Tab 切换 + 数据加载 | 搜索输入、滑块联动重计算 |
与 Suspense 的配合(消除 fallback 闪烁,kp-020 误区的解法):
jsxfunction Tab({ tab }) {
const [isPending, startTransition] = useTransition();
function select(next) {
startTransition(() => setCurrentTab(next));
}
return (
<>
{tabs.map(t => (
<button key={t.id} onClick={() => select(t.id)}
aria-current={t.id === tab}>
{t.label} {t.id === tab && isPending ? '…' : ''}
</button>
))}
<Suspense fallback={null}> {/* 过渡期间沿用旧内容,不闪骨架 */}
<TabContent tab={tab} />
</Suspense>
</>
);
}
过渡更新期间,React 保留已显示的旧 UI 继续可交互,新 UI 就绪后整体切换——这就是「不闪烁」的机制。
直观类比
useTransition 像餐厅「已下单,厨艺中」的小票:客人(用户)的催单(输入)立即被记录,厨房(React)先把催单回显出来,大菜(重渲染列表)慢慢烧,小票上的「进行中」(isPending)告诉客人别急。useDeferredValue 像电视的插播字幕:新闻正文(value)实时更新,下方的滚动条(deferred)晚一拍跟上——没人会觉得字幕卡了新闻。debounce 则是「每 3 秒才允许点一次菜」,快是快,但顾客的手被限制住了。
实例 / 案例
组合拳:搜索页的完整体验优化(kp-019 案例的进阶版):
jsxfunction SearchPage({ items }) {
const [query, setQuery] = useState('');
const deferred = useDeferredValue(query);
const isStale = query !== deferred; // 手工"pending"感
const filtered = useMemo(
() => items.filter(i => i.name.toLowerCase().includes(deferred.toLowerCase())),
[items, deferred]
);
return (
<>
<input value={query} onChange={e => setQuery(e.target.value)} autoFocus />
<ul className={isStale ? 'opacity-60' : ''}>
{filtered.map(i => <Row key={i.id} item={i} />)}
</ul>
</>
);
}
输入零卡顿(query 立即回显)、结果自动错峰、过期结果半透明提示——三件事各一行代码。
常见误区
- 把输入框自己的 setState 包进 transition:输入回显是最高优先级,包了反而让打字「追不上手」。规则:只有「结果展示」的更新进过渡。
- 把 transition 当性能优化器:它不省工作量(kp-022 误区);大数据列表该虚拟化还是虚拟化。
- useDeferredValue 传了新对象:deps 按引用比较,内联对象每次都是「新值」,延迟机制失效;传原始值或 useMemo 的引用。
- 以为 isPending 能区分「哪个更新在进行」:它是整个过渡队列的开关灯;复杂场景拆多个 state 分组进不同 transition。
- 滥用 isPending 全屏 loading:过渡的本意是「旧 UI 继续可用」;全屏遮罩等于自废武功。
自测题
- 用一句话区分两个 API 的控制对象;什么时候必须用 useTransition(而不能用 useDeferredValue)?
- 为什么输入框 setState 不能放进 startTransition?写出正确拆分。
- useDeferredValue 与 debounce 在「快速连续输入」时的行为差异?
- 过渡期间 Suspense 为什么不该显示 fallback?React 显示的是什么?
- 把 kp-023 案例里
query !== deferred的原理讲清楚(两次渲染各用什么值)。
与其他知识点的关系
- 向前:kp-022 的用户级出口。
- 向后:kp-025 的 Actions 自动采用过渡优先级提交表单更新;kp-019 把它们纳入性能手段对照表;kp-020 的闪烁问题在此获得标准解。
延伸阅读
- useTransition:<https://react.dev/reference/react/useTransition>
- useDeferredValue:<https://react.dev/reference/react/useDeferredValue>
- Keep the UI responsive with transitions(官方教程):<https://react.dev/learn/react-api-reference#transitions>