useTransition 和 useDeferredValue 有什么用?区别?

一、是什么

useTransitionuseDeferredValue 是 React 18 引入的两个并发特性 Hook,它们的核心目的相同:将某些更新标记为低优先级,从而保证高优先级更新(如用户输入)的流畅响应

两者的根本区别在于作用对象不同:

  • useTransition:将一个状态更新操作标记为非紧急
  • useDeferredValue:将一个已有的值延迟更新

二、useTransition

2.1 基本用法

import { useState, useTransition } from 'react';

function TabContainer() {
  const [tab, setTab] = useState('home');
  const [isPending, startTransition] = useTransition();

  function handleTabClick(nextTab) {
    startTransition(() => {
      setTab(nextTab);
    });
  }

  return (
    <div>
      <nav>
        {['home', 'posts', 'settings'].map(t => (
          <button
            key={t}
            onClick={() => handleTabClick(t)}
            style={{ fontWeight: tab === t ? 'bold' : 'normal' }}
          >
            {t}
          </button>
        ))}
      </nav>
      {isPending && <div className="loading-bar" />}
      <TabContent tab={tab} />
    </div>
  );
}

useTransition 返回一个数组:

  • isPending:布尔值,表示 transition 是否正在等待(可用于显示加载指示)
  • startTransition:函数,将其内部的状态更新标记为 transition(低优先级)

2.2 搜索过滤场景

这是 useTransition 最典型的应用场景——输入框保持流畅,列表渲染延迟更新:

import { useState, useTransition, memo } from 'react';

const SlowList = memo(function SlowList({ filter }) {
  const items = [];
  for (let i = 0; i < 5000; i++) {
    const text = `Item ${i}`;
    if (text.toLowerCase().includes(filter.toLowerCase())) {
      items.push(<li key={i}>{text}</li>);
    }
  }
  return <ul>{items}</ul>;
});

function SearchPage() {
  const [query, setQuery] = useState('');
  const [filter, setFilter] = useState('');
  const [isPending, startTransition] = useTransition();

  function handleChange(e) {
    const value = e.target.value;
    setQuery(value); // 高优先级:立即更新输入框

    startTransition(() => {
      setFilter(value); // 低优先级:可中断的列表更新
    });
  }

  return (
    <div>
      <input value={query} onChange={handleChange} placeholder="搜索..." />
      <div style={{ opacity: isPending ? 0.6 : 1 }}>
        <SlowList filter={filter} />
      </div>
    </div>
  );
}

2.3 与 Suspense 配合

startTransition 与 Suspense 有特殊的交互:在 transition 期间触发的 Suspense 不会立即显示 fallback,而是保持显示旧内容:

import { useState, useTransition, Suspense, lazy } from 'react';

const PostsPage = lazy(() => import('./PostsPage'));
const SettingsPage = lazy(() => import('./SettingsPage'));

function App() {
  const [page, setPage] = useState('home');
  const [isPending, startTransition] = useTransition();

  function navigate(nextPage) {
    startTransition(() => {
      setPage(nextPage);
    });
  }

  return (
    <div>
      <nav>
        <button onClick={() => navigate('posts')}>文章</button>
        <button onClick={() => navigate('settings')}>设置</button>
      </nav>
      <Suspense fallback={<div>页面加载中...</div>}>
        <div style={{ opacity: isPending ? 0.7 : 1 }}>
          {page === 'home' && <h1>首页</h1>}
          {page === 'posts' && <PostsPage />}
          {page === 'settings' && <SettingsPage />}
        </div>
      </Suspense>
    </div>
  );
}

三、useDeferredValue

3.1 基本用法

import { useState, useDeferredValue } from 'react';

function SearchResults({ query }) {
  const deferredQuery = useDeferredValue(query);

  return <HeavyList filter={deferredQuery} />;
}

useDeferredValue 接收一个值,返回该值的"延迟版本"。当传入的值变化时:

  1. React 先用旧值完成一次渲染(保证立即响应)
  2. 然后在后台用新值重新渲染(低优先级,可被中断)
  3. 新值渲染完成后,屏幕更新为新结果

3.2 完整的搜索示例

import { useState, useDeferredValue, useMemo } from 'react';

function SearchApp() {
  const [query, setQuery] = useState('');

  return (
    <div>
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="输入搜索词..."
      />
      <SearchResults query={query} />
    </div>
  );
}

function SearchResults({ query }: { query: string }) {
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;

  const filteredItems = useMemo(() => {
    return generateLargeList().filter(item =>
      item.name.toLowerCase().includes(deferredQuery.toLowerCase())
    );
  }, [deferredQuery]);

  return (
    <div style={{ opacity: isStale ? 0.5 : 1, transition: 'opacity 0.2s' }}>
      <p>找到 {filteredItems.length} 个结果</p>
      <ul>
        {filteredItems.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

3.3 延迟渲染子树

useDeferredValue 的一个强大模式是延迟整个组件子树的渲染:

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

const Chart = memo(function Chart({ data }) {
  // 渲染一个复杂的图表,非常耗时
  return (
    <svg viewBox="0 0 800 400">
      {data.map((point, i) => (
        <circle key={i} cx={point.x} cy={point.y} r={3} />
      ))}
    </svg>
  );
});

function Dashboard() {
  const [range, setRange] = useState([0, 100]);
  const deferredRange = useDeferredValue(range);

  return (
    <div>
      <RangeSlider value={range} onChange={setRange} />
      <Chart data={computeChartData(deferredRange)} />
    </div>
  );
}

注意:useDeferredValue 需要配合 memouseMemo 才能真正跳过不必要的渲染。如果子组件没有 memo,每次父组件渲染都会导致子组件重新渲染,deferred value 就失去了意义。

四、两者的核心区别

4.1 控制权归属

// useTransition:你控制状态更新
function Parent() {
  const [data, setData] = useState(initial);
  const [isPending, startTransition] = useTransition();

  function update(newData) {
    startTransition(() => {
      setData(newData); // 你决定哪个更新是 transition
    });
  }

  return <Child data={data} />;
}

// useDeferredValue:你不控制状态更新,但延迟消费值
function Child({ data }) {
  const deferredData = useDeferredValue(data);
  return <HeavyComponent data={deferredData} />;
}

4.2 对比表

特性useTransitionuseDeferredValue
作用对象状态更新(setter)已有的值
返回值[isPending, startTransition]延迟后的值
适用场景自己触发的状态更新来自 props 或外部的值
加载指示内置 isPending需手动比较新旧值
是否包裹 setState
与 Suspense 交互避免立即显示 fallback同样避免

4.3 选择指南

你能控制触发状态更新的代码吗?
  ├── 是 → useTransition
  │       • 你在调用 setState
  │       • 你需要 isPending 状态
  │       • 例:Tab 切换、搜索触发
  └── 否 → useDeferredValue
          • 值来自 props
          • 值来自第三方 Hook
          • 例:接收父组件传入的搜索词

五、进阶用法

5.1 startTransition(非 Hook 版本)

React 还导出了模块级的 startTransition 函数,可在组件外使用,但不提供 isPending

import { startTransition } from 'react';

document.getElementById('btn').addEventListener('click', () => {
  startTransition(() => {
    root.render(<NewPage />);
  });
});

5.2 React 19 扩展

  • startTransition 支持异步函数(Actions),可在 transition 中执行 async/await
  • useDeferredValue 增加了 initialValue 参数:useDeferredValue(query, '')

5.3 性能优化要点

  1. memo 包裹耗时组件,避免不必要的渲染
  2. useMemo 缓存计算结果,确保 deferred/transition 值未变时不重新计算
  3. 将输入状态和搜索状态分离,输入框始终即时响应

六、总结

useTransitionuseDeferredValue 是 React 并发渲染的核心消费 API。它们不会让渲染本身变快,而是让 React 学会"先做重要的事"——确保用户感知到的界面始终保持响应。选择哪一个取决于你是否控制状态更新的源头:控制源头用 useTransition,只消费值用 useDeferredValue