Skip to content

useDeferredValue 与响应预算 ​

问题:只有一个查询 state,怎样让慢列表晚一点追赶 ​

有时你只拥有 query,无法或不想维护第二份 state。输入框应读最新 query,昂贵子树可以读 deferredQuery。React 会先用旧延迟值完成紧急渲染,再在后台尝试包含新值的渲染。

完整实验:陈旧结果仍可见 ​

jsx
import { memo, useDeferredValue, useState } from 'react'

const records = Array.from({ length: 300 }, (_, id) => ({
  id,
  title: `Record ${String(id).padStart(4, '0')}`,
}))

function SlowRow({ record, query }) {
  const start = performance.now()
  while (performance.now() - start < 1) {} // 仅用于实验
  return record.title.toLowerCase().includes(query) ? <li>{record.title}</li> : null
}

const SlowResults = memo(function SlowResults({ query }) {
  return <section>
    <p>显示查询:{query || '空'}</p>
    <ul>{records.map(record =>
      <SlowRow key={record.id} record={record} query={query.toLowerCase()} />,
    )}</ul>
  </section>
})

export default function App() {
  const [query, setQuery] = useState('')
  const deferredQuery = useDeferredValue(query)
  const isStale = query !== deferredQuery

  return (
    <main>
      <h1>记录搜索</h1>
      <input aria-label="查询" value={query} onChange={(event) => setQuery(event.target.value)} />
      <div
        aria-busy={isStale}
        style={{ opacity: isStale ? 0.55 : 1, transition: 'opacity 160ms' }}
      >
        {isStale && <p>结果正在更新,当前仍显示旧查询。</p>}
        <SlowResults query={deferredQuery} />
      </div>
    </main>
  )
}

第一次渲染中两个值相同。输入变化后,React 先尝试用新 query 和旧 deferredQuery 渲染,因此输入立即回显、结果带陈旧提示;随后在后台尝试让 deferredQuery 追上。若又输入字符,后台工作可被丢弃并从新值重来。

memo 很重要:父组件紧急渲染时,props 仍是旧 deferredQuery,慢子树可跳过;没有 memo,子组件仍会随父组件执行,效果可能不明显。React Compiler 可自动处理部分记忆化,但课程示例显式写出边界。

慢工作分布在各个 SlowRow,React 可以在组件工作之间响应更紧急更新。一次 filter 里进行几百毫秒计算仍是不可中断的同步调用,延迟值不会把它拆开。这里也不能把 <SlowRow /> 改成直接调用函数;组件边界与 props 不变时的跳过条件,分别承担“可以让出工作”和“紧急路径不用重做”的责任。

延迟值由 Object.is 比较。若每次渲染都构造 { query } 再交给 useDeferredValue,即使查询没变,对象身份也变了,会安排多余后台工作。优先延迟 query 这样的原始值;确需对象时,先按第三章的规则稳定其身份。已经处在 Transition 中的更新本来就是非紧急的,此时 useDeferredValue 可直接返回新值,不需要再套一轮延迟。

它不是定时器 ​

useDeferredValue 没有固定延迟,React 会根据其他工作调度。它不同于防抖:每个新值仍可能进入后台渲染,也不会减少网络请求。若需求是“停止输入 300ms 后才请求”,应实现明确防抖或使用数据层策略;若需求是“输入优先、旧结果暂时可读”,延迟值更贴切。

关键变体与预算 ​

  • 延迟值可与 Suspense 配合:新结果 suspend 时继续显示旧结果,并用 isStale 降低透明度。
  • 背景渲染的 Effect 只有提交后才运行;被放弃的树不会运行 Effect。
  • 先用 React Profiler、浏览器 Performance 或输入延迟观察建立基线。小列表没卡顿时,引入 memo/延迟只增加复杂度。
  • 延迟不能修复 O(n²) 算法或过大的 DOM;先减少工作量,再安排剩余工作。

练习:契约任务 ​

把数据改为书名数组并实现:

  1. 输入立即显示 typed: <query>,结果显示 shown: <deferredQuery>。
  2. 匹配忽略首尾空格和大小写;空查询显示全部。
  3. 两值不同时设置 aria-busy="true" 并显示陈旧提示,相同后消失。
  4. 把匹配提取为纯函数,并用断言覆盖空查询、大小写和无结果;不得把延迟值当作减少请求次数的方案。

完成后对照答案 05-03。

用心记录每一步成长