useDeferredValue 给你一个允许滞后的值的副本。当值变化时,React 会先用旧的副本重绘屏幕,让输入保持即时响应,然后在后台用新值渲染缓慢的部分。
快速输入一个单词。输入框跟得上你的速度,而列表落后时会变暗。Console 会为每个字母打印一行 SlowList rendering,因为 React 一开始渲染 SlowList,它就会打印,但中间那些值对应的列表从未显示到屏幕上:每输入一个新字母都会中断后台渲染,React 再用最新的文本重新开始。把 query={deferredQuery} 改成 query={query} 再输入一次:现在每次按键都要等列表渲染四分之一秒。
语法
const deferredValue = useDeferredValue(value);
const deferredValue = useDeferredValue(value, initialValue); // React 19
value是你要渲染的任何东西:来自输入框的字符串、数字、对象。原始类型的值效果最好,因为每次渲染都新建的对象总会被视为发生了变化。- 返回值大多数时候等于
value。在value刚刚变化后的一次渲染中,它仍然保留着之前的值,随后在一次后台渲染中追上来。
工作原理
每次变化都会产生两次渲染:
- 一次紧急渲染,使用新的
value和旧的deferredValue。输入框显示新的文本。只接收deferredValue的组件拿到的 props 和之前相同。 - 一次后台渲染,使用新的
deferredValue。这是慢的那次。React 会把它分片进行渲染,如果在它完成之前value又变了,就放弃这次渲染,再用最新的值重新开始。
把两个值都打印出来,就能看到这个过程:
输入一个字母,比如 a,Console 会再打印两行:先是 text = "a" 加上 deferred = "",然后两者都是 "a"。第一次渲染正是让输入保持流畅的关键,但前提是缓慢的部分能跳过它。这就是第一个示例中 SlowList 要用 memo 包裹的原因:在紧急渲染期间,它的 query prop 没有变化,所以 React 会跳过它。没有 memo,紧急渲染仍然会把整个缓慢的列表跑一遍,只不过用的是旧文本,你什么也得不到。
显示过期的内容
后台渲染进行期间,value 和延迟值是不同的。比较两者,就能在不隐藏内容的情况下告诉用户内容已经过期:
const isStale = query !== deferredQuery;
<div style={{ opacity: isStale ? 0.5 : 1, transition: 'opacity 0.2s' }}>
<SlowList query={deferredQuery} />
</div>
这通常比加载动画更友好:旧的结果依然可读,新结果准备好后就地替换。同样的思路也适用于 Suspense:如果延迟值让某个组件在加载数据时挂起,React 会继续显示旧的内容而不是后备内容,直到新内容准备好(见 Suspense 和 lazy)。
初始值(React 19)
默认情况下,第一次渲染没有可以滞后的对象,所以延迟值从一开始就是真实值。从 React 19 开始,你可以传入第二个参数。第一次渲染时,Hook 返回 initialValue,随后 React 在后台用真实值再渲染一次。这样你就能先立即显示一些开销小的内容,紧接着再补上开销大的部分。
Console 显示了两次渲染:第一次是 "",然后是完整的文本。去掉第二个参数,就只会打印一次渲染,直接就是完整文本。
useDeferredValue 与 useTransition
两者都把工作标记为非紧急,并使用同一套渲染机制。区别在于你把它们放在哪里:
- useTransition 包裹的是更新。你调用
startTransition(() => setQuery(text)),所以需要拿到 setter。isPending会直接提供给你。 - useDeferredValue 包裹的是值。你在使用这个值的地方调用它,所以当值作为 prop 传进来,或来自一个你无法控制的 Hook 时,它也能用。“等待中”的状态需要你自己用
value !== deferredValue计算。
// You own the state: either works, a transition is direct
startTransition(() => setQuery(nextQuery));
// You only receive the value
function SearchResults({ query }) {
const deferredQuery = useDeferredValue(query);
return <SlowList query={deferredQuery} />;
}
useDeferredValue 与防抖和节流
防抖会在最后一次变化后等待一段固定的时间;节流则在每个时间间隔内最多更新一次。两者都要预先选定一个延迟,所以在速度快的笔记本上,它们会让页面比实际需要的更慢,而在速度慢的手机上,这个延迟又可能仍然太短。
useDeferredValue 没有延迟。后台渲染会立即开始,所以在速度快的设备上,列表几乎立刻就会更新;在速度慢的设备上,React 会保持输入流畅,并尽可能频繁地渲染列表。后台工作还是可以中断的,而防抖后的渲染则不行:防抖后的渲染一旦开始,就会一直阻塞直到完成。
它做不到的,是减少网络请求。如果每个新值都会发起一次请求,那么延迟值仍会随着每次完成的渲染而变化,所以当请求数量很重要时,要对请求本身做防抖。
常见错误
- 忘了
memo。 正如上面所示,缓慢的组件必须能够跳过紧急渲染。如果它没有用memo包裹,或者它的其他某个 prop 每次渲染都是新对象,那么每次按键它都会渲染两次,而不是一次。 - 传入一个新对象。
useDeferredValue({ query })每次渲染都会创建一个新对象,所以延迟值看起来总是变了,React 每次都会多启动一次后台渲染。应该延迟原始类型的值(useDeferredValue(query)),再基于它构造对象。 - 指望请求变少。 根据延迟值请求数据的组件,仍然会为每个完成渲染的值发起请求。
什么时候该用它
当一个输入驱动着页面上渲染缓慢的一部分、这部分可以 memo 化、而且你能接受短暂显示稍旧的结果时,就用 useDeferredValue:搜索结果、根据筛选条件重绘的图表、格式化文本的预览。如果缓慢的部分之所以慢,是因为某个你能修复的原因,比如只有 50 行可见却渲染了 5,000 行,就先修复那个问题。如果你能拿到 setter,这种模式的另一种写法请看 useTransition。
常见问题
useDeferredValue 有什么作用?
它返回一个可能落后于真实值的版本。当值变化时,React 会先用旧的延迟值渲染,再在后台用新值渲染一次,而这次后台渲染是可以被中断的。
为什么 useDeferredValue 没有让我的列表变快?
缓慢的组件必须用 memo 包裹,并且接收的是延迟值。否则紧急渲染仍然会渲染这个缓慢的组件,只不过用的是旧值,输入依然会卡。
useDeferredValue 和 useTransition 有什么区别?
useTransition 包裹的是 state 更新,所以你需要拿到 setter。useDeferredValue 包裹的是值,所以当你只能拿到这个值时它也能用,比如值作为 prop 传进来,或者来自一个你无法控制的 Hook。
useDeferredValue 是一种防抖吗?
不是。它没有固定的延迟。React 会在紧急渲染之后立即开始延迟渲染,在速度快的设备上几乎看不出滞后。它本身也不会减少网络请求。
useDeferredValue 的第二个参数是什么?
从 React 19 开始,你可以传入一个 initialValue。第一次渲染时 Hook 返回它,随后 React 会在后台用真实值再渲染一次。