陌上人如玉
公子世无双

2.10 useTransition 与 useDeferredValue

2.10 useTransition 与 useDeferredValue

一句话:两个 Hook 都基于「并发渲染」——把「不紧急的更新」降优先级,让「紧急更新(如输入回显)」先完成;useTransition 用于你主动触发的状态更新,useDeferredValue 用于被动接收的值。

一、背景:为什么要区分优先级

// ❌ 问题:一个「输入框 + 大列表过滤」的组件
function Search() {
  const [keyword, setKeyword] = useState('');
  const [list, setList] = useState(BIG_LIST);

  // 每次输入都要过滤 10000 条数据 → 主线程被占满 → 输入卡顿
  const filtered = list.filter((item) => item.name.includes(keyword));

  return (
    <>
       setKeyword(e.target.value)} />
      {filtered.map((it) => )}
    
  );
}
// 用户感受:每敲一个字都要等几百毫秒,输入框「一顿一顿」的

// 根本问题:React 18 之前,所有更新都是「同步的、同优先级的」:
//   setKeyword → 渲染开始 → 必须一口气渲染完(包括昂贵的列表)
//   → 输入框的回显也被这个长渲染阻塞
并发渲染(Concurrent Rendering)提供的解法:
① 把更新分成「紧急」与「非紧急」
   · 紧急(输入回显、hover 反馈)→ 必须立刻响应
   · 非紧急(列表过滤、图表重算)→ 可以稍后、可以被中断
② 渲染过程可以【中断】,让浏览器先处理更高的优先级
③ React 会在「空闲时」继续未完成的工作,并且可以【丢弃过期的渲染】

⚠️ 注意:默认情况下 React 仍然把「普通 setState」当作紧急更新。
   要区分优先级,必须显式使用这两个 Hook(或 startTransition)。

二、useTransition

const [isPending, startTransition] = useTransition();

// 用法:把「非紧急的更新」包进 startTransition
setKeyword(input);                       // ① 紧急:立即更新(输入框回显)
startTransition(() => {
  setFilteredList(computeFiltered(input));  // ② 非紧急:可以被打断
});
// ✅ 改造上面的搜索组件
function Search() {
  const [keyword, setKeyword] = useState('');
  const [filtered, setFiltered] = useState(BIG_LIST);
  const [isPending, startTransition] = useTransition();

  const handleChange = (e: React.ChangeEvent) => {
    const value = e.target.value;

    setKeyword(value);                     // 紧急更新:输入框立刻回显
    startTransition(() => {                 // 非紧急更新:列表过滤可以延后
      setFiltered(BIG_LIST.filter((it) => it.name.includes(value)));
    });
  };

  return (
    
{/* 用 isPending 提示「正在更新」 */}
); } // 用户感受:输入立刻响应,列表稍后跟上(可以配一个轻微的 loading 态)

返回值:

返回值 含义
isPending 是否有「正在进行的 transition(非紧急更新)」
startTransition 把回调里的状态更新标记为非紧急
// 多个 transition 共存时,isPending 是「是否有任一 transition 在进行」
const [isPending, startTransition] = useTransition();

const switchTab = (tab: string) => {
  startTransition(() => {
    setActiveTab(tab);           // 两个更新都是非紧急
    setTabData(computeTabData(tab));
  });
};

return (
  <>
    
    {isPending && }    {/* 切换时显示轻量 loading */}
    
  
);

startTransition 也可以从 react 直接导入(不需要 Hook):

import { startTransition } from 'react';

// 用在「事件处理之外」的场景(如路由跳转、外部数据源回调)
router.push(path);                       // 紧急:导航要立即响应
startTransition(() => {
  setPageContent(computeContent(path));  // 非紧急:内容计算可以延后
});

三、useDeferredValue

const deferredValue = useDeferredValue(value);
// 返回一个「滞后版本的 value」:紧急更新时它保持旧值,等空闲后再更新
// ✅ 用 useDeferredValue 改造(不改造「更新逻辑」,而是「延迟使用某个值」)
function Search() {
  const [keyword, setKeyword] = useState('');

  // ⭐ 不需要改 setKeyword 的写法,只在「使用」时用延迟版本
  const deferredKeyword = useDeferredValue(keyword);

  // 用滞后值做昂贵计算
  const filtered = useMemo(
    () => BIG_LIST.filter((it) => it.name.includes(deferredKeyword)),
    [deferredKeyword]
  );

  const isStale = keyword !== deferredKeyword;   // ⭐ 判断「正在显示旧结果」

  return (
    <>
       setKeyword(e.target.value)} />
      
); }

与 useTransition 的关键差异:

维度 useTransition useDeferredValue
作用对象 你触发的状态更新(包住 setState) 传入的值(延迟它的使用)
能否控制更新代码 ✅ 需要把 setState 放进回调 ❌ 不需要(改动更小)
获取 pending 状态 ✅ isPending ❌ 需要自己比较(value !== deferred)
适用场景 你能控制「更新在哪里发生」 值来自 props / 外部,你改不了源头
能否用于「非 state 的值」 — ✅(如 props、context 值)
// useDeferredValue 的典型不可替代场景:值来自 props(无法改源头)
function List({ searchQuery }: { searchQuery: string }) {
  // ❌ 不能用 useTransition:这里的渲染由父组件触发,我们控制不了 setState
  // ✅ 用 useDeferredValue 延迟使用这个值
  const deferredQuery = useDeferredValue(searchQuery);
  const results = useMemo(() => filter(deferredQuery), [deferredQuery]);

  return 
    {results.map((r) =>
  • {r.name}
  • )}
; }
// 另一个用途:延迟渲染「昂贵的子树」(相当于「选择性 hydration」的简化版)
function Page({ data }) {
  const deferredData = useDeferredValue(data);

  return (
    <>
      
{/* 紧急:立刻用最新数据 */} {/* 非紧急:用滞后数据 */} ); }

四、与防抖/节流的区别

防抖/节流(debounce / throttle):
  ① 是「时间维度」的控制——延迟固定的毫秒数
  ② 与 React 的渲染机制无关(纯 JS 定时器)
  ③ 结果:输入停止后 X 毫秒才更新
  ④ 优点:能真正「减少」计算次数(跳过中间状态)
  ⑤ 缺点:有固定延迟(不够「跟手」),且需要管理定时器

useTransition / useDeferredValue:
  ① 是「优先级维度」的控制——让高优先级先完成
  ② 依赖 React 的并发渲染能力
  ③ 结果:紧急更新立刻生效,非紧急更新「尽快跟进」(通常比防抖快)
  ④ 优点:没有固定延迟,输入与结果可以「渐进对齐」
  ⑤ 缺点:不能减少总计算量(该算的还是会算),只是「不阻塞紧急更新」
维度 防抖 useDeferredValue
控制维度 时间 优先级
是否有固定延迟 ✅(如 300ms) ❌(由浏览器空闲时间决定)
减少计算次数 ✅(跳过中间输入) ❌(每次都会算,但可被打断)
适合 发请求(避免每次输入都请求) 同步的昂贵渲染(本地过滤/重算)
组合 两者可以一起用(请求用防抖、渲染用 deferred)
// 组合使用:输入防抖发请求 + deferred 渲染
function Search() {
  const [keyword, setKeyword] = useState('');
  const [results, setResults] = useState([]);

  // ① 请求用防抖(减少网络请求)
  const debouncedKeyword = useDebounce(keyword, 300);

  useEffect(() => {
    if (!debouncedKeyword) return;
    let cancelled = false;
    api.search(debouncedKeyword).then((data) => { if (!cancelled) setResults(data); });
    return () => { cancelled = true; };
  }, [debouncedKeyword]);

  // ② 渲染用 deferred(保持输入流畅)
  const deferredResults = useDeferredValue(results);
  const isStale = results !== deferredResults;

  return (
    <>
       setKeyword(e.target.value)} />
      
{deferredResults.map((r) => )}
); }

五、更多适用场景

// ① Tab 切换(大内容块)
function Tabs({ tabs }: { tabs: Tab[] }) {
  const [active, setActive] = useState(tabs[0]!.key);
  const [isPending, startTransition] = useTransition();

  const switchTab = (key: string) => {
    startTransition(() => setActive(key));    // 切换时旧的 tab 内容先留着
  };

  return (
    <>
      
      
); } // ② 路由跳转(配合框架) function useNavigateTransition() { const navigate = useNavigate(); const [isPending, startTransition] = useTransition(); const go = (to: string) => { startTransition(() => navigate(to)); // 跳转不阻塞当前页面的交互 }; return { go, isPending }; } // ③ 延迟「昂贵的第三方组件」渲染(如编辑器、图表) function Editor({ content }: { content: string }) { const deferredContent = useDeferredValue(content); return ; }

六、常见坑

// ① 用 useTransition 包住「紧急更新」
startTransition(() => setInputValue(e.target.value));   // ❌ 输入回显变成非紧急 → 卡顿
// ✅ 只有「非紧急」的更新才放进 transition

// ② 把「需要立即反应的交互反馈」放进 transition
startTransition(() => setLoading(true));                // ❌ 用户会看不到 loading
startTransition(() => setHovered(true));                // ❌ hover 反馈必须紧急

// ③ 认为 transition 能「减少计算量」
// → 它只是「不阻塞紧急更新」,计算该做还是要做

// ④ 在 transition 内部依赖「更新后的 state」
startTransition(() => {
  setList(filter(keyword));       // ⚠️ keyword 是当前渲染的闭包值,不是「更新后」的值
});

// ⑤ 用 transition 包住「受控输入」
startTransition(() => setText(value));   // ❌ 输入框会「卡字」

// ⑥ isPending 用得过重(如全屏 loading 遮罩)
// → 会让用户觉得「更慢」,应该用轻量的视觉提示(透明度、小 spinner)

// ⑦ 认为 useDeferredValue 会「跳过中间值」
// → 它只是「延迟使用」,中间值仍会渲染,只是优先级低

// ⑧ 用 useDeferredValue 替代「防抖发请求」
const d = useDeferredValue(keyword);
useEffect(() => { fetch(d); }, [d]);     // ⚠️ 请求次数不会减少(只是时机稍晚)
// ✅ 请求应该用防抖,渲染用 deferred

// ⑨ 在 transition 里做副作用(不应该)
startTransition(() => { api.post(data); });   // ❌ 副作用应该同步执行

// ⑩ 认为「加了 transition 就一定流畅」
// → 如果「紧急更新本身」就很重(如输入框所在组件要渲染大列表),依然会卡

七、原理简述

背后的机制(简化):

① React 18 引入了「车道(Lane)」模型:
   每个更新会被分配一个优先级车道(SyncLane / TransitionLane / ...)
② startTransition 把其中的更新标记为「非紧急」(TransitionLane)
③ 渲染时 React 按「优先级从高到低」处理:
   高优先级车道可以先渲染、并【中断】正在进行的低优先级渲染
④ 低优先级渲染被打断后,React 会「稍后重试」(如果期间状态又变了,
   会【丢弃】已作废的渲染,只算最新的)
⑤ 因此「紧急更新」总能立刻反映到屏幕上

useDeferredValue 的实现也是基于同一机制:
  它相当于「用一个 transition 更新一个内部状态」,
  并且会尝试「复用上一次的渲染结果」(所以实现上更轻量)

前提:并发特性需要 createRoot(React 18+ 的新入口,即 ReactDOM.createRoot)。如果用旧的 ReactDOM.render,这些特性不生效。

八、面试延伸

  1. 「useTransition 和 useDeferredValue 有什么区别?」
    useTransition 作用于「你主动触发的状态更新」——把 setState 包进 startTransition 回调,并返回 isPending 让你知道「后台更新是否在进行」;useDeferredValue 作用于「传入的值」——返回该值的一个「滞后版本」,你在渲染昂贵部分时使用它。前者适合「你能控制更新代码」的场景,后者适合「值来自 props/外部、改不了源头」的场景。

  2. 「它们和防抖、节流有什么区别?」
    防抖/节流是时间维度的控制(延迟固定毫秒数),能减少计算次数(跳过中间输入),适合发请求;useTransition/useDeferredValue 是优先级维度的控制(让紧急更新先完成,非紧急更新可被打断),不减少计算量,适合同步的昂贵渲染。实践上两者常组合:请求用防抖,渲染用 deferred。

  3. 「为什么说它们依赖 React 18 的并发渲染?」
    因为它们的能力来自「可中断的渲染 + 优先级调度」:React 18 用「车道模型」给每个更新分配优先级,渲染时可以中断低优先级的渲染、先处理高优先级,并在之后重试或丢弃过期的渲染。这套机制要求使用 createRoot(React 18 的新入口),在旧的 ReactDOM.render 下这些特性不生效。

  4. 「什么更新应该放进 startTransition?」
    所有「非紧急且可能耗时」的更新:大列表的过滤/排序结果、切换到内容很重的 Tab、路由跳转后的内容渲染、昂贵图表的数据更新、富文本/编辑器内容同步。不该放:输入框的受控值(setInputValue)、hover/focus 反馈、loading 状态、任何「用户期待立刻看到」的反馈——这些放进 transition 反而会造成卡顿。

  5. 「isPending 一般怎么用?」
    用它做「轻量的过渡提示」,而不是「全屏 loading」。常见做法:① 把内容区域降低透明度(opacity: 0.6)表示「正在更新」;② 顶部显示一条细进度条;③ 显示一个小小的 spinner。关键是要轻——如果用全屏遮罩,用户会觉得更慢、更卡,与优化目标相反。

  6. 「useDeferredValue 是怎么实现的?」
    它的原理与 useTransition 同源:React 内部会维护一个「延迟的值」,当传入的值变化时,它先用旧值继续渲染(保持界面稳定),然后在低优先级的更新中把延迟值更新为新值。实现上它相当于「用一个 transition 更新一个内部状态」,并且会尽量复用上一次的渲染结果(因此在某些场景比 useTransition 更轻量)。

  7. 「为什么不直接给列表加防抖?」
    因为防抖解决的是「减少执行次数」,而渲染卡顿的根因是「一次渲染太长」——即使不防抖、每次都渲染,只要这次渲染可以被中断、不阻塞输入,用户感受就很好。而且防抖会引入固定延迟(300ms 后才更新结果),在快速输入时显得不跟手。两者定位不同:防抖减少「执行次数」,transition 降低「执行优先级」。

  8. 「用了这两个 Hook 页面就一定流畅吗?」
    不一定。它们只解决「非紧急更新阻塞紧急更新」这一种问题。如果:① 紧急更新本身就很重(比如输入框所在组件必须渲染一个大列表),那依然会卡;② 卡顿来自别的原因(DOM 节点过多、布局抖动、网络等待),也帮不上忙;③ 过度的 transition 反而会让界面「滞后感」变重。所以仍应先用 Profiler 定位问题,再决定是否使用它们。

一句话速记

两者都基于 React 18 的并发渲染,把「非紧急更新」降优先级,让「输入回显」这类紧急更新不被阻塞;useTransition 包住你触发的 setState(返回 isPending),useDeferredValue 延迟你接收到的值(适合 props 来源、改不了源头);不减少计算量(只是不阻塞紧急更新),与「防抖减少次数」定位不同,请求用防抖、渲染用 deferred;输入受控值、hover/loading 反馈不能放进 transition;isPending 只做轻量提示(透明度、细进度条),别用全屏遮罩;需要 createRoot 才生效。

赞(0) 打赏
未经允许不得转载:陌上寒 » 2.10 useTransition 与 useDeferredValue

评论 抢沙发

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

微信扫一扫

支付宝扫一扫