React并发模式怎么用?详解useTransition与useDeferredValue的使用场景与区别。在传统的React渲染模型里,一旦组件开始更新,整个渲染过程就是不可中断的。如果某次更新涉及一个非常大的列表,渲染可能耗费几百毫秒,这期间用户的点击、输入统统得不到响应,界面看起来就像卡死了一样。React 18引入并发模式之后,这个问题有了新的解法:更新可以被标记成不同优先级,高优先级的更新可以打断低优先级的渲染,用户交互始终能第一时间得到反馈。而实现这套机制最常用的两个Hook,就是useTransition和useDeferredValue。

一、先理解渲染阻塞问题:为什么需要并发模式
假设页面上有一个搜索框和一个包含两万条数据的列表,用户在输入框里打字时,每敲一个字符都会触发状态更新,列表随之重新过滤渲染。同步渲染模式下,React必须把这轮渲染完整做完才能处理下一个事件,当过滤逻辑加渲染本身耗时超过16毫秒甚至更久时,用户连续输入就会明显卡顿,每敲一个键要等几百毫秒才显示出来。
React 18的并发渲染核心思路是“可中断的渲染”。React内部会把更新任务切分成多个小单元,在渲染过程中如果发现有更高优先级的更新进来(比如用户又敲了一个键),它会丢弃当前进行到一半的低优先级渲染,优先处理高优先级任务。这样用户输入始终丝滑,代价是那些“重”的计算被延后了。
需要注意,并发模式并不改变你的组件代码写法,它改变的是React调度更新的方式。并发特性目前是渐进式启用的,也就是只有使用了useTransition、useDeferredValue这类Hook的状态更新才会享受并发调度,其余更新行为和以前一致,这也是官方推荐的落地方式。
二、useTransition:把更新标记为低优先级过渡任务
useTransition返回一个数组,第一个元素isPending是一个布尔值,表示过渡任务是否还在进行中,第二个元素startTransition是一个函数,你把昂贵的状态更新放进它的回调里,这个更新就会被标记为transition级别的低优先级任务。来看一个搜索联想的完整示例:
import { useState, useTransition, memo } from 'react';
// 列表项用 memo 包裹,减少不必要的重渲染
const ListItem = memo(function ListItem({ item }) {
return <li>{item}</li>;
});
export default function SearchList({ allItems }) {
const [inputValue, setInputValue] = useState('');
const [searchText, setSearchText] = useState('');
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
// 高优先级更新:输入框立即响应
setInputValue(e.target.value);
// 低优先级更新:列表过滤交给过渡任务
startTransition(() => {
setSearchText(e.target.value);
});
};
const filtered = allItems.filter((it) => it.includes(searchText));
return (
<div>
<input value={inputValue} onChange={handleChange} />
{isPending && <span>加载中...</span>}
<ul>
{filtered.slice(0, 100).map((item) => (
<ListItem key={item} item={item} />
))}
</ul>
</div>
);
}
这段代码的关键在于把两个状态拆开:inputValue负责输入框的显示,属于高优先级更新,用户每敲一个键立刻反映;searchText驱动列表过滤,被包在startTransition里,即使渲染很慢也不会阻塞输入。isPending为true时你可以显示一个加载提示,告诉用户结果正在更新,体验上比整个页面冻住要好得多。
使用上有几个细节值得注意。第一,startTransition里的更新必须是能触发重新渲染的状态更新,如果只是修改ref或者调用外部函数,标记优先级没有意义。第二,isPending表示的是“过渡中的更新尚未渲染完成”,不是网络请求状态,别把它当成loading来管理异步请求。第三,transition更新如果被打断,React会丢弃中间结果直接用最新值重新渲染,所以不会出现界面显示过期搜索结果的问题。
三、useDeferredValue:延迟渲染一个昂贵的值
useDeferredValue的适用场景略有不同。有时你无法控制状态更新发生的位置——比如输入框组件是第三方库的,它内部自己管理value,你只能拿到变化后的值。这时useDeferredValue就派上用场了:传入一个值,它返回这个值的“延迟版本”,当真实值变化时,组件先按旧值快速渲染一轮,重活儿留到后面。
import { useState, useDeferredValue, memo } from 'react';
const HeavyList = memo(function HeavyList({ query }) {
// 模拟昂贵的过滤与渲染
const items = [];
for (let i = 0; i < 20000; i++) {
if (String(i).includes(query)) {
items.push(<li key={i}>{i}</li>);
}
}
return <ul>{items}</ul>;
});
export default function App() {
const [query, setQuery] = useState('');
// deferredQuery 是 query 的延迟副本
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<div style={{ opacity: isStale ? 0.6 : 1 }}>
<HeavyList query={deferredQuery} />
</div>
</div>
);
}
这里的query更新是正常优先级,输入框始终即时响应;而HeavyList拿到的是deferredQuery,当用户连续快速输入时,它会先保持旧值渲染,等高优先级更新处理完,再以最新值重新渲染。通过比较query和deferredQuery是否相等,还能知道当前展示的是不是过期数据,进而做透明度降低之类的视觉提示,让用户感知到列表正在追赶。
useDeferredValue的底层逻辑其实和useTransition是同一套调度机制,可以粗略理解为:useTransition是“让你控制更新在哪里发生”,useDeferredValue是“让你控制值在哪里被延迟消费”。官方文档也提到,useDeferredValue(value)在效果上近似于startTransition包裹的一套受控状态更新,只是抽象层级不同。
四、两者怎么选:适用场景对比与实战注意点
选择标准很直接:如果你能控制状态的设置过程(setState在你能改的代码里),优先用useTransition,因为它语义更明确,还能拿到isPending状态做加载提示;如果值来自你控制不了的地方,比如组件库内部的受控值、URL参数、父组件传下来的prop,那就用useDeferredValue来延迟它的消费。
两者都只是“优先级调度”,不是“性能优化银弹”。如果列表渲染慢是因为组件本身写得有问题,比如每次渲染都重建两万个没有memo的节点,那并发机制只能让输入不卡,列表更新本身依然慢。正确做法是配合memo、useMemo把子组件的渲染开销降下来,再叠加并发调度,效果才理想。另外,被延迟的更新被打断后是丢弃重来的,如果过滤逻辑本身计算量极大,频繁打断反而会造成重复计算,这种情况建议配合防抖或者把计算移到useMemo中缓存。
最后提醒一点:并发特性要求项目使用React 18以上版本,且最好通过createRoot API挂载应用。如果还在用旧的ReactDOM.render,这些Hook虽然不会报错,但行为会退化成同步执行,起不到任何优化作用。升级后建议先用React DevTools的Profiler验证过渡任务是否真的被标记为低优先级,确认调度生效后再推广到更多页面。
React并发模式useTransitionuseDeferredValue修改时间:2026-09-08 19:11:00