React Hook useDefferedValue
一、基本定义与作用
- 定位:React 内置 Hook,用于将部分 UI 渲染标记为 “后台任务”,实现非阻塞更新
- 核心价值:优先保障输入、点击等紧急交互的即时响应,将低优先级任务(如列表过滤、路由切换渲染)延后执行,避免页面卡顿
- 返回值:
[isPending, startTransition]isPending:布尔值,标记是否存在待处理的 transition 任务startTransition:函数,用于包裹需标记为低优先级的状态更新逻辑
二、关键用法与场景
1. 执行非阻塞更新
核心逻辑:将不紧急的状态更新(如数据请求后的状态同步)包裹在
startTransition中,避免阻塞用户交互示例
const [quantity, setQuantity] = useState(1); const [isPending, startTransition] = useTransition(); const updateQuantityAction = async (newQuantity) => { startTransition(async () => { const savedQuantity = await updateQuantity(newQuantity); // 异步请求 startTransition(() => setQuantity(savedQuantity)); // 标记为低优先级更新 }); };
2. 显示待处理状态
利用
isPending:在 transition 执行期间,通过isPending展示加载反馈(如按钮状态、加载标识),提升用户感知示例
function TabButton({ action, children }) { const [isPending, startTransition] = useTransition(); return ( <button onClick={() => startTransition(async () => await action())}> {isPending ? <b className="pending">加载中...</b> : children} </button> ); }
3. 避免不必要的加载指示器
- 配合 Suspense 使用:在路由切换、数据加载场景,用 transition 标记状态更新,避免 Suspense 全局 fallback 频繁触发(仅在局部显示加载状态)
- 典型场景:选项卡切换时,仅在按钮显示 “pending”,而非替换整个选项卡容器为加载态
4. 构建 Suspense 路由
在没加
useTransition时,Suspense 的工作逻辑是:- 当组件触发数据加载(如用
use加载路由组件、用Suspense包裹数据请求组件); - React 会立即将 Suspense 包裹的整个区域 替换为
fallback(比如全局的 “转圈加载动画”); - 直到数据加载完成,才替换回真实内容。
- 当组件触发数据加载(如用
这种默认行为的问题在于:
- 如果加载区域很大(比如整个页面、整个选项卡容器),会导致 “全局闪烁”—— 用户看到整个区域被加载态覆盖,体验生硬;
- 加载期间无法中断(比如用户刚点了 “选项卡 A”,又想改点 “选项卡 B”,但加载态已经出来,只能等 A 加载完才能切 B)。
路由导航优化:将页面切换的状态更新标记为 transition,实现:
- 导航可中断:用户能 “反悔”,不用等加载完,不会打断用户交互
- 场景:用户先点 “商品页”(触发加载),100ms 后发现点错,想改点 “购物车”。
- 没有
useTransition:必须等 “商品页” 加载完,才能切换到 “购物车”(加载过程不可停); - 有
useTransition:因为 “切换商品页的状态更新” 被标记为低优先级,当用户再点 “购物车” 时(高优先级交互),React 会 中断 “商品页” 的加载任务,立即开始处理 “购物车” 的加载,用户不用等。
- 避免全局加载闪烁:加载时不覆盖整个页面
- 场景:从 “首页”(已加载完成)切换到 “商品页”(需加载)。
- 没有
useTransition:整个页面被 Suspense 的 fallback(全局加载动画)覆盖,首页内容消失; - 有
useTransition:React 会 继续显示首页内容,直到 “商品页” 的组件和数据加载完成,再 “无缝替换” 成商品页 —— 整个过程没有 “全白 + 加载” 的闪烁,用户感知不到 “加载间隙”。
- 等待副作用完成后再显示新页面:避免 “半成品页面”
- 场景:“商品页” 需要加载两个东西:① 商品页组件本身;② 商品列表数据(副作用)。
- 没有
useTransition:可能出现 “组件先加载完,但数据还没到” 的情况,导致页面显示 “空列表 + 加载”(局部加载态,仍需用户等); - 有
useTransition:React 会 等 “组件 + 数据” 所有副作用都完成后,再一次性将首页切换为商品页,用户看到的直接是 “完整的商品页”,不用等两次加载。
- 导航可中断:用户能 “反悔”,不用等加载完,不会打断用户交互
示例
function Router() { const [page, setPage] = useState('/'); const [isPending, startTransition] = useTransition(); const navigate = (url) => startTransition(() => setPage(url)); // 路由更新标记为低优先级 // ... 路由匹配逻辑 }
5. 错误边界结合
- 错误捕获:
startTransition中抛出的错误,可通过错误边界(Error Boundary)捕获,向用户展示友好提示 - 要求:需将调用
useTransition的组件包裹在错误边界内
三、重要特性与限制
1. 核心特性
- 不阻塞交互:仅修改状态更新优先级,不中断当前执行的高优先级任务(如输入、点击)
- 同组件内生效:仅能标记当前组件可访问的
set函数(状态更新),若需响应 prop 变化,可配合useDeferredValue startTransition稳定性:函数标识稳定,可省略 Effect 依赖数组(不触发额外重渲染)- 可中断性:低优先级更新可被高优先级任务(如输入)打断,后续自动恢复
2. 关键限制
- 不可用于输入控制:不能标记输入框(如
<input>)的状态更新,否则会导致输入延迟、同步异常 - 异步更新需二次包裹:
await后的状态更新需重新用startTransition包裹(当前 React 限制,未来将优化) - 无顺序保证:多次异步 transition 可能因请求竞态导致更新顺序错乱,需自行处理(或用
useActionState等高级抽象) - 同步执行函数:
startTransition包裹的函数会立即执行,仅内部状态更新被标记为低优先级(非延迟执行)
四、常见问题与解决方案
| 问题场景 | 原因 | 解决方案 |
|---|---|---|
| 输入框更新卡顿 | 误将输入控制状态标记为 transition | 1. 拆分状态:输入状态同步更新,其他逻辑用低优先级状态;2. 用 useDeferredValue 延迟非输入相关状态 |
| 状态更新未被标记为 transition | 1. 更新在 startTransition 外;2. 用 setTimeout 包裹更新 | 1. 确保更新在 startTransition 函数内部;2. 将 setTimeout 放在 startTransition 外,内部包裹更新 |
await 后更新不生效 | React 无法跟踪异步上下文,await 后失去 transition 标记 | await 后重新调用 startTransition 包裹状态更新 |
组件外部调用 useTransition | useTransition 是 Hook,仅能在组件 / 自定义 Hook 内调用 | 使用独立的 startTransition 函数(无 isPending 状态) |
五、问题总结
startTransition 只能保证当前事件链内的更新是“低优先级的并发更新”。 await 会把事件链切断,所以如果希望某段异步流程始终保持统一的 pending 状态,就必须在 await 之后重新 startTransition。以 官方文档UseTransition 的Action 与常规事件处理的区别中的例子为参考,必须通过三个startTransition:
- 第一个
startTransition在<item />这个子组件中,标记着标记用户操作开始,触发 pending 状态。 - App 内部异步请求的
startTransition:将网络请求纳入同一 transition,使 pending 覆盖整个异步过程,而不会因为await打断事件链,导致前一个transition的流程提前结束,造成UI闪烁。 - App 内部 setState 的 startTransition:保证异步完成后的状态更新也属于同一 transition,避免 UI 闪烁。
总结:
- 每个 startTransition 都有明确作用,缺一都会导致 pending 状态不准确或 UI 闪烁。
- Item 的 startTransition 触发全局 pending,但 await 后的更新必须在 App 内再次 startTransition 才能保持一致。