在 React 18 中,并发渲染(Concurrent Rendering)让 UI 更新有了优先级的概念。useTransition 和 useDeferredValue 就是两个帮助我们标记低优先级更新的 Hook,它们的核心作用是保持界面交互的流畅性,防止耗时计算阻塞用户操作。
为什么需要优先级控制
假设你有一个搜索框,用户输入时需要在下方展示筛选后的列表。如果列表很长,筛选计算又重,每次按键都会触发一次耗时的渲染,导致输入框出现明显卡顿——这是典型的“输入阻塞渲染”问题。
在并发模式下,React 会将输入更新(用户正在打字)视为高优先级,将列表更新视为低优先级。当两者同时发生时,React 可以中断低优先级的渲染,优先处理高优先级的更新,处理完后再继续低优先级的渲染。这样用户打字始终流畅。
useTransition 和 useDeferredValue 就是让开发者显式告诉 React:“这个更新不重要,可以滞后处理”的工具。
useTransition:标记某个状态更新为“过渡任务”
useTransition 返回一个包含两个值的数组:
isPending:布尔值,表示低优先级更新是否正在等待完成(可用于显示加载状态)。startTransition:函数,将包裹在其中的状态更新标记为低优先级。
基本用法:
import { useState, useTransition } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
const value = e.target.value;
setQuery(value); // 高优先级:立即更新输入框的文字
startTransition(() => {
// 低优先级:延迟更新筛选后的列表
const filtered = performHeavyFilter(value);
setResults(filtered);
});
};
return (
<div>
<input value={query} onChange={handleChange} placeholder="搜索..." />
{isPending && <span>计算中...</span>}
<ResultsList data={results} />
</div>
);
}
这里的关键点:
setQuery(value)在startTransition外部执行,React 认为它是紧急更新(用户打字),会立即同步处理,保证输入框跟手。setResults(filtered)被包裹在startTransition内,成为了低优先级更新。如果用户在列表渲染完成前又输入了新字符,React 会放弃当前渲染,直接用新的value重新计算并渲染。isPending可以用来向用户展示一个友好的“正在处理”状态,避免因列表短暂未更新而产生疑惑。
不使用 useTransition 会怎样?
同样场景,如果直接将 setResults 放在事件处理中(与 setQuery 相同优先级),React 会把两个状态更新合并处理,导致输入框的更新会被复杂计算卡住。使用 useTransition 后,输入框永远流畅,列表最多就是延迟一点出现。
适用场景:
- 搜索框输入时更新搜索结果。
- 切换 Tab 页时立即切换按钮高亮,但内容区可以稍后渲染。
- 实时过滤长列表或图表。
useDeferredValue:延迟使用某个值
useDeferredValue 接收一个值,返回一个“延迟版本”。当原始值发生变化时,延迟值会保持旧值一段时间,直到浏览器空闲或更高优先级的工作完成,再更新为新值。
基本用法:
import { useState, useDeferredValue } from 'react';
function ProductList({ products, filterText }) {
const deferredFilter = useDeferredValue(filterText);
// 使用延迟后的值进行计算,避免每次输入都重新过滤
const filteredProducts = useMemo(
() => products.filter(p => p.name.includes(deferredFilter)),
[products, deferredFilter]
);
return (
<ul>
{filteredProducts.map(p => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}
这里与 useTransition 的区别在于,useTransition 是由你主动包裹更新来降低优先级,而 useDeferredValue 是让某个值的更新自动变为低优先级,更方便与现有代码集成。
什么时候用 useTransition,什么时候用 useDeferredValue?
- 你能控制状态更新的地方(比如事件处理函数内),使用
useTransition,因为它还能提供isPending反馈状态。 - 如果某个值来自第三方组件、或是现有库的状态,你不方便改写状态更新代码,则用
useDeferredValue来“延迟消费”它。
两者的协作示例
一个典型搜索场景,结合两者:
function SearchPage({ list }) {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
setQuery(e.target.value); // 立即更新输入框
startTransition(() => {
// 低优先级更新(但这里其实可以被 useDeferredValue 替代)
// 仅用于展示 isPending
});
};
const filteredList = useMemo(
() => list.filter(item => item.text.includes(deferredQuery)),
[list, deferredQuery]
);
return (
<div>
<input value={query} onChange={handleChange} />
{isPending && <small>正在更新...</small>}
<ItemList items={filteredList} />
</div>
);
}
需要注意的几点
- 这些 Hook 仅在并发模式下有效(React 18 默认启用并发特性),必须使用
createRoot渲染。 - 不要将
startTransition用于需要立即反馈的操作(如表单提交),那应该保持同步更新。 useDeferredValue返回的值可能在一段时间内是“过时”的,因此要确保你的 UI 可以安全地使用旧数据(比如仅用于展示,不会导致误操作)。- 渲染过程中的重计算仍然会消耗 CPU,
useTransition不能优化计算本身,只是调整了计算的时机和顺序,避免阻塞高优先级任务。
总结
useTransition 和 useDeferredValue 是 React 并发模式下用于精细控制 UI 响应优先级的工具。它们让输入框这类紧急交互不再被复杂的列表渲染所拖累,大幅提升了用户体验。在实际开发中,凡是遇到“输入反馈慢”“切换不流畅”的场景,都可以优先考虑使用这两个 Hook 来优化。