Skip to main content

useTransition/DeferredValue

两个都用来「让某些更新不那么紧急」,区别在使用方式:一个是包裹更新动作,一个是延迟某个值。

一、包裹一次更新动作​

它解决的不是「慢」,而是「响应性」:总耗时一点没变,但输入不再被大渲染卡住。做法是把触发重渲染的那次更新标记为「可中断」,React 会在渲染过程中让位给更紧急的更新(比如继续输入),之后再重新渲染。所以它不是延迟执行——更新立刻开始,只是可以被打断重来。

const [pending, startTransition] = useTransition()
const [keyword, setKeyword] = useState('')
const [list, setList] = useState(all)

function onChange(e) {
setKeyword(e.target.value) // 紧急:输入框要立刻更新
startTransition(() => setList(filter(all, e.target.value))) // 可中断
}

进行中的 transition 被新更新打断时会直接丢弃重算,不提交半成品——所以过渡期间不会闪现中间态。记住:总耗时没变,变的是输入不被卡住。这也解释了为什么 transition 适合输入框过滤而不是按钮提交:输入是连续的高频事件,每一次都该让位给下一次;而按钮提交是一次性事件,包进 transition 反而可能延迟它本该立刻发生的反馈。

二、延迟一个值​

它包裹的是那个值,不是更新代码。值来自父组件或外部、你改不了它的来源,就用它拿到一个滞后版本:UI 先按旧值渲染,随后再跟上新值。适合筛选值在 props 里、你没法去包它的 setState 的场景。

function SearchResults({ keyword }: { keyword: string }) {
const deferred = useDeferredValue(keyword) // keyword 来自父级,这里改不了来源
const list = useMemo(() => filter(all, deferred), [deferred])
return <List items={list} />
}

它和 useTransition 是不同入口,不是竞争关系:一个控制「我发起的更新」,一个控制「我收到的那个值」。注意 useDeferredValue 拿到的是「上一个值」的滞后副本,期间如果源值又变了,它会继续追赶——所以用它展示大列表时,列表会短暂显示旧结果再刷新,这是预期行为,不是 bug。

三、按触发源选​

一句话区分:useTransition 包裹的是触发更新的那段代码(更新由我发起,我知道它是非紧急的);useDeferredValue 包裹的是那个值(值来自父组件或外部,我改不了它的来源)。判断标准就是「我能不能控制这次 setState」。

你的情况用哪个
能拿到触发 setState 的代码useTransition
只能拿到一个值,来源在上游useDeferredValue

四、中断与重放的行为​

渲染可中断并重新开始,用 transition 的更新不会阻塞紧急更新,且不会显示中间态闪烁。但副作用不能放在 transition 里——发请求、写库这类动作不该被反复打断重来,它们应该在 action 或事件回调里执行,transition 只负责「界面跟不跟得上」。transition 适合「界面跟不跟得上」的优化,不适合「请求发不发得对」——发请求该用 action 的 pending 状态来表达,而不是用 transition 兜底。另一个常见误用是把首屏关键内容放进 transition,结果首屏反而变慢:transition 让位给「更紧急」的更新,如果首屏本身就是最紧急的,标成可中断只会让它排到后面。

它解决的是渲染卡顿,不是接口慢。这两者的处理方式完全不同:

// 慢的是渲染(大列表过滤)→ transition 有用
startTransition(() => setKeyword(v))

// 慢的是请求(接口 3 秒才回)→ transition 只会让等待更不明显,问题还在
// 该做的是:加缓存、加骨架屏、把请求提前到 hover 时发出
紧急更新输入输入输入transition渲染被打断完成低优先级的渲染一遇到新的输入就作废重来,所以它「让位」,而不是「变快」。
图:transition 的中断与重放——被让位的是它,先响应的是输入

五、别用它掩盖慢请求​

几点:transition 内不要做同步阻塞,否则该让位的渲染自己卡住了;isPending 只用于禁用按钮或显示骨架,不要当成业务状态判断;transition 不是防抖——它不减少执行次数,要减少请求次数仍然要防抖。输入框的即时反馈不要套 transition,否则反而有卡顿感。

// isPending 只做禁用/骨架,不做业务判断
<button disabled={pending}>提交</button>
{pending && <Skeleton />}
// 要减少请求次数,仍要靠防抖,transition 管不了这个
// transition 与防抖分工:前者管优先级,后者管次数
function onChange(e) {
const v = e.target.value
debouncedSearch(v) // 减少请求次数
startTransition(() => setList(filter(all, v))) // 不阻塞输入
}

六、它不让渲染变快​

transition 不是防抖。它不减少执行次数,只调整优先级;要减少请求次数仍然得靠防抖。

用了也不代表不卡——渲染本身很慢时照样卡,它保证的是紧急更新能插队。

isPending 也不能拿来当业务状态用,它只表示过渡还没完成,适合禁用按钮或显示骨架,不适合作为业务分支条件。

记住一句就够了——能控制 setState 就用 useTransition,控制不了就用 useDeferredValue。

两个 Hook 的语义区别见 react.dev:useTransition,官方对「标记非紧急更新」的解释比二手总结准确。