
一个慢,一个必须快
搜索框里每敲一个字,下面一万条数据的列表就重渲染一次,输入就变得一顿一顿。
useDeferredValue 给这个值配一份慢半拍的副本:
import { useState, useDeferredValue } from 'react'
function SearchPage() { const [query, setQuery] = useState('') const deferredQuery = useDeferredValue(query)
return ( <> <input value={query} onChange={e => setQuery(e.target.value)} /> <SlowList text={deferredQuery} /> </> )}输入框用 query,立刻更新;列表用 deferredQuery,晚一步跟上来。
它到底做了什么
新值到来时,React 先用旧值渲染一次(所以你看到的还是旧结果),然后在后台用新值再渲染一次。
后台那次渲染可被打断:用户继续敲字,React 放弃它,用最新的值重新开始。设备快的时候它几乎立刻完成,看不出差别;设备慢的时候列表自然落后,不需要你挑一个延迟数字。
React 19 起多了第二个参数
const deferredQuery = useDeferredValue(query, '')签名是 useDeferredValue(value, initialValue?)。首个渲染用 initialValue;不传时首次渲染不做延迟,因为当时没有「上一版」可以显示。首屏就要拿一份内容垫着时,这个参数有用。
配合 Suspense 不闪加载态
<Suspense fallback={<h2>加载中……</h2>}> <SearchResults query={deferredQuery} /></Suspense>输入变化时,旧结果留在屏幕上,直到新结果就绪。用户不会看到 fallback 一闪而过。
这里要提醒一句:它不减少请求。每敲一个字,请求照样发出去,被推迟的是「把结果换上屏幕」这件事。要减少请求,用防抖或节流。
标出「内容有点旧」
const isStale = query !== deferredQuery
<div style={{ opacity: isStale ? 0.5 : 1 }}> <SearchResults query={deferredQuery} /></div>一开始输入,旧结果就变淡;新结果到了恢复原样。
慢组件必须先包 memo
这条容易漏,官方特意标了出来:只有慢组件被 memo 包住,优化才成立。
原因是 deferredQuery 没变时,React 仍然会渲染父组件;父组件一执行,props 就是新建的。列表没包 memo,props 每次都不同,它照样重渲染,延迟值等于白设。
还有个结构上的要求:繁重的计算要放在慢组件渲染时做。放在事件处理函数里的开销是同步执行的,延迟值救不了它。
与防抖、节流的区别
防抖是「等用户停下来再更新」,节流是「最多每秒更新一次」,两者都要你挑一个延迟数字。
useDeferredValue 不设延迟,跟着设备快慢自适应。更关键的是它可被打断:React 正在渲染那张长列表时用户又敲了一下,它会放弃这次渲染、先处理输入。防抖与节流做不到,它们只是把阻塞往后推,卡顿依旧存在。
需要减少网络请求时,防抖与节流仍然有用,两者可以一起用。
四条边界
传进去的应该是原始类型,或者渲染之外创建的对象。渲染中新建一个对象再传进去,每次都是新引用,会引发多余的后台渲染。
更新已经在过渡里(被 startTransition 包着)时,它直接返回新值,不再另起后台渲染。
后台渲染提交到屏幕之前不会触发 Effect。
别用它给正在输入的值本身做延迟。输入框的值必须同步更新,能延迟的是下游消费这个值的那部分界面。
分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
换条路线
从其他文章中稳定抽取
最后更新于 ,距今已过 190 天
部分内容可能已过时