跳转到主要内容
Hook18.0+

useTransition

标记非紧急的状态更新,让 React 在并发渲染中保持界面响应性

核心概述

痛点: UI 阻塞问题

在 React 18 之前,所有状态更新都是"紧急"的(Urgent),这导致:

  • 输入卡顿: 当用户在输入框打字时,如果同时有大量列表渲染, 会导致输入延迟(用户按键 → 字符显示在屏幕上有明显延迟)
  • 点击无响应: 点击按钮后,如果触发重型计算或渲染, 界面会"冻结",用户无法看到点击反馈
  • 动画掉帧: 正在进行的动画会因为其他高优先级更新而中断

解决方案: 并发渲染与 Transition

React 18 引入并发渲染(Concurrent Rendering), 允许 React 同时准备多个版本的 UI,根据优先级选择显示哪个版本。

useTransition 利用并发渲染能力,将状态更新标记为"非紧急"(Non-urgent):

  • 优先级分级: 区分紧急更新(如打字、点击)和非紧急更新(如搜索结果、Tab 切换)
  • 可中断渲染: 非紧急更新可以被更高优先级的更新打断,保持界面响应
  • 并发特性: React 在浏览器空闲时计算 transition 更新,不影响用户交互

适用场景

  • 搜索/过滤: 输入框立即更新,搜索结果延迟计算
  • Tab 切换: 点击立即响应,内容延迟加载
  • 列表展开: 展开/折叠图标立即变化,子节点延迟渲染
  • 数据可视化: 过滤器立即应用,图表延迟重绘

💡 心智模型

将 useTransition 想象成"快递加急通道":

  • 加急通道(紧急更新): 用户打字、点击等高优先级任务走加急通道, 立即处理并显示
  • 普通通道(Transition): 搜索结果、Tab 内容等非紧急任务走普通通道, 在加急任务空闲时处理
  • 可插队机制: 如果加急通道有新任务,普通通道的任务暂停, 优先处理加急任务
  • 完成标志: isPending 告诉你普通通道是否有任务正在处理

关键: transition 中的更新不是"异步执行"(像 setTimeout), 而是"低优先级执行"(可以在浏览器空闲时处理,但会被紧急更新打断)。

技术规格

类型签名

function useTransition(): [
  boolean,
  (callback: () => void) => void
]

返回值

返回值类型说明
isPendingboolean是否有 transition 正在处理。true 表示有 pending transition
startTransition(callback: () => void) => void标记状态更新为 transition 的函数,接受一个包含 setState 调用的回调

运行机制

useTransition 基于 React 18 的并发渲染特性,底层机制如下:

  • 优先级调度: React 使用 Lane 模型管理更新优先级, transition 更新被分配较低的优先级
  • 时间切片: React 将渲染工作分解成小单元, 在每个时间切片后检查是否有更高优先级的更新
  • 可中断渲染: 如果在 transition 渲染过程中有紧急更新(如用户输入), React 会放弃当前的 transition 渲染,处理紧急更新后再重新开始 transition
  • 并发协调: React 可以同时维护多个版本的 UI 树, 根据优先级选择显示哪个版本

注意: useTransition 需要 React 18+ 和支持并发渲染的渲染器 (如 ReactDOM 18+ 的 createRoot)。使用旧版 ReactDOM.render() 不会启用并发特性。

实战演练

示例 1: 搜索输入框(基础用法)

最常见的场景:用户输入立即显示,搜索结果延迟计算。

效果: 用户打字时,输入框立即响应,即使 items 数组很大(如 10,000 条), 也不会感到卡顿。搜索结果会在浏览器空闲时更新。

示例 2: Tab 切换(生产级,结合 Suspense)

Tab 切换是另一个典型场景:点击立即响应,内容延迟加载。

效果: 点击 Tab 按钮时,按钮样式立即变化(用户得到反馈), 内容区域显示加载状态,数据加载完成后显示内容。整个过程流畅,不会"卡住"界面。

示例 3: 树形列表展开(复杂场景)

树形结构展开时,可能需要渲染大量子节点。使用 transition 可以保持交互流畅。

效果: 点击展开图标时,图标立即旋转(用户得到反馈), 子节点在浏览器空闲时渲染。即使有数千个子节点,也不会阻塞界面。

示例 4: 数据可视化仪表板(生产级)

在数据可视化中,过滤器和图表都很"重",使用 transition 分离紧急和非紧急更新。

效果: 用户调整过滤器时,UI 立即响应(下拉框关闭、输入框显示值), 数据过滤、图表重绘在后台进行。即使数据量大、计算复杂,界面始终保持可交互。

避坑指南

❌ 陷阱 1: 将文本输入框的更新标记为 Transition

问题: 文本输入需要立即更新,否则会感觉"延迟"或"卡顿"。 将输入更新放在 transition 中会导致打字不跟手。

// ❌ 错误: 文本输入不应该用 transition
function SearchInput() {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    startTransition(() => {
      setQuery(e.target.value); // ❌ 会导致打字延迟!
    });
  };

  return <input value={query} onChange={handleChange} />;
}

// ✅ 正确: 文本输入立即更新,搜索结果延迟更新
function SearchInput({ items }: { items: string[] }) {
  const [query, setQuery] = useState('');
  const [filteredItems, setFilteredItems] = useState(items);
  const [isPending, startTransition] = useTransition();

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

    // ✅ 立即更新输入框
    setQuery(value);

    // ✅ 搜索结果用 transition
    startTransition(() => {
      const filtered = items.filter(item =>
        item.toLowerCase().includes(value.toLowerCase())
      );
      setFilteredItems(filtered);
    });
  };

  return (
    <div>
      <input value={query} onChange={handleChange} />
      {isPending ? <Spinner /> : <Results items={filteredItems} />}
    </div>
  );
}

心智模型纠正: 文本输入是"紧急更新"(用户需要立即看到反馈), 只有搜索结果、Tab 内容等才是"非紧急更新"。

❌ 陷阱 2: 在 Transition 回调中执行副作用

问题: Transition 回调应该只包含状态更新, 不应该包含副作用(如直接修改 DOM、调用 API、记录日志等)。

// ❌ 错误: 在 transition 中执行副作用
function TabContainer() {
  const [tab, setTab] = useState('home');
  const [isPending, startTransition] = useTransition();

  const switchTab = (newTab: string) => {
    startTransition(() => {
      setTab(newTab);
      // ❌ 不要在 transition 中做这些:
      document.title = newTab; // 直接修改 DOM
      console.log('Switched to', newTab); // 记录日志
      fetch('/api/track', { // API 调用
        method: 'POST',
        body: JSON.stringify({ tab: newTab })
      });
    });
  };

  return <div>...</div>;
}

// ✅ 正确: 副作用放在 useEffect 中
function TabContainer() {
  const [tab, setTab] = useState('home');
  const [isPending, startTransition] = useTransition();

  const switchTab = (newTab: string) => {
    startTransition(() => {
      setTab(newTab); // ✅ 只包含状态更新
    });
  };

  // ✅ 副作用放在 useEffect 中
  useEffect(() => {
    document.title = tab;
  }, [tab]);

  useEffect(() => {
    console.log('Switched to', tab);
    fetch('/api/track', {
      method: 'POST',
      body: JSON.stringify({ tab })
    });
  }, [tab]);

  return <div>...</div>;
}

心智模型纠正: Transition 只用于标记"状态更新的优先级", 不用于管理"副作用"。副作用应该放在 useEffect 中。

❌ 陷阱 3: 期望 Transition 减少计算量或渲染次数

问题: Transition 不会减少计算量或跳过渲染, 它只是调整渲染的"优先级"和"时机",让紧急更新可以先处理。

// ❌ 错误理解: 认为 transition 会"跳过"计算
function ExpensiveList({ items }: { items: Item[] }) {
  const [filter, setFilter] = useState('');
  const [filtered, setFiltered] = useState(items);
  const [isPending, startTransition] = useTransition();

  const handleChange = (value: string) => {
    setFilter(value);

    startTransition(() => {
      // ❌ 错误理解: 以为这样会"减少"计算
      // 实际上: 完整的过滤计算还是会执行,只是优先级较低
      const filtered = items.filter(item =>
        item.name.toLowerCase().includes(value.toLowerCase())
      );
      setFiltered(filtered);
    });
  };

  return <div>...</div>;
}

// ✅ 正确理解: Transition 保持响应性,不减少计算
// 如果真的要减少计算,应该用防抖、useMemo 或 Web Worker
function ExpensiveList({ items }: { items: Item[] }) {
  const [filter, setFilter] = useState('');
  const [filtered, setFiltered] = useState(items);
  const [isPending, startTransition] = useTransition();

  // 方案 1: 使用 useMemo 缓存计算结果
  const filteredMemo = useMemo(() => {
    return items.filter(item =>
      item.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [items, filter]);

  // 方案 2: 使用防抖减少计算频率
  const debouncedFilter = useDebounce(filter, 300);

  useEffect(() => {
    const filtered = items.filter(item =>
      item.name.toLowerCase().includes(debouncedFilter.toLowerCase())
    );
    setFiltered(filtered);
  }, [items, debouncedFilter]);

  // 方案 3: Transition + useMemo(推荐)
  const handleChange = (value: string) => {
    setFilter(value); // 立即更新

    startTransition(() => {
      // ⏳ 延迟更新,但计算量没变
      // 好处: 用户可以继续输入,不会感觉卡顿
      const filtered = items.filter(item =>
        item.name.toLowerCase().includes(value.toLowerCase())
      );
      setFiltered(filtered);
    });
  };

  return <div>...</div>;
}

心智模型纠正: Transition 是"优先级管理工具",不是"性能优化工具"。 它让界面保持响应,但不减少计算量。要真正减少计算,需要用防抖、useMemo、Web Worker 等。

❌ 陷阱 4: 过度使用 Transition

问题: 不是所有状态更新都需要 transition。 简单的、快速的更新用 transition 反而增加复杂度。

// ❌ 错误: 简单更新也用 transition
function SimpleButton() {
  const [count, setCount] = useState(0);
  const [isPending, startTransition] = useTransition();

  const handleClick = () => {
    startTransition(() => {
      // ❌ 这个更新很快,不需要 transition
      setCount(c => c + 1);
    });
  };

  return (
    <button onClick={handleClick}>
      {isPending ? '更新中...' : `点击了 ${count} 次`}
    </button>
  );
}

// ✅ 正确: 简单更新直接更新
function SimpleButton() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    // ✅ 简单的计数器更新,不需要 transition
    setCount(c => c + 1);
  };

  return <button>点击了 {count} 次</button>;
}

// ✅ 正确: 只有复杂的、可能阻塞的更新才用 transition
function ComplexButton({ items }: { items: Item[] }) {
  const [selectedItem, setSelectedItem] = useState<Item | null>(null);
  const [details, setDetails] = useState<Details | null>(null);
  const [isPending, startTransition] = useTransition();

  const handleItemClick = (item: Item) => {
    // ✅ 立即更新选中状态(视觉反馈)
    setSelectedItem(item);

    // ✅ 延迟计算详细信息(可能很耗时)
    startTransition(() => {
      const details = calculateExpensiveDetails(item);
      setDetails(details);
    });
  };

  return (
    <div>
      {items.map(item => (
        <div
          key={item.id}
          onClick={() => handleItemClick(item)}
          className={selectedItem === item ? 'selected' : ''}
        >
          {item.name}
        </div>
      ))}
      {isPending && <Spinner />}
      {details && <DetailsPanel details={details} />}
    </div>
  );
}

心智模型纠正: Transition 用于"可能阻塞交互的复杂更新", 简单的、快速的更新直接用 setState 即可。

最佳实践

✅ 推荐模式

1. 分离紧急和非紧急更新

这是使用 useTransition 的核心原则:将直接影响用户体验的更新(输入、点击反馈) 和可以延迟的更新(搜索结果、内容加载)分开。

const handleInput = (value: string) => {
  // ✅ 紧急更新: 用户需要立即看到
  setInputValue(value);

  // ✅ 非紧急更新: 可以延迟
  startTransition(() => {
    setFilteredData(filterData(value));
  });
};

2. 结合 Suspense 显示加载状态

Transition 与 Suspense 配合使用,可以优雅地处理异步数据加载。

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

  const switchTab = (newTab: string) => {
    startTransition(() => {
      setTab(newTab);
    });
  };

  return (
    <div>
      <button onClick={() => switchTab('posts')}>文章</button>
      {isPending && <Spinner />}

      <Suspense fallback={<div className="p-4">加载中...</div>}>
        {tab === 'home' && <Home />}
        {tab === 'posts' && <Posts />}
      </Suspense>
    </div>
  );
}

3. 使用 isPending 显示加载状态

利用 isPending 为用户提供视觉反馈,告诉他们"系统正在处理"。

function SearchInput({ items }: { items: string[] }) {
  const [query, setQuery] = useState('');
  const [filteredItems, setFilteredItems] = useState(items);
  const [isPending, startTransition] = useTransition();

  const handleChange = (value: string) => {
    setQuery(value);
    startTransition(() => {
      const filtered = items.filter(item =>
        item.toLowerCase().includes(value.toLowerCase())
      );
      setFilteredItems(filtered);
    });
  };

  return (
    <div>
      <input value={query} onChange={(e) => handleChange(e.target.value)} />
      {/* ✅ 显示加载状态 */}
      {isPending && (
        <div className="mt-2 text-sm text-gray-500 flex items-center gap-2">
          <Spinner className="w-4 h-4" />
          <span>搜索中...</span>
        </div>
      )}
      <ul className="mt-2">
        {filteredItems.map(item => (
          <li key={item}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

4. 与其他性能优化工具配合

Transition 可以与 useMemo、useCallback、防抖等工具配合使用, 达到最佳性能。

function OptimizedSearch({ items }: { items: Item[] }) {
  const [query, setQuery] = useState('');
  const [filteredItems, setFilteredItems] = useState(items);
  const [isPending, startTransition] = useTransition();

  // ✅ 使用防抖减少计算频率
  const debouncedQuery = useDebounce(query, 300);

  // ✅ 防抖后再用 transition 保持响应性
  useEffect(() => {
    startTransition(() => {
      const filtered = items.filter(item =>
        item.name.toLowerCase().includes(debouncedQuery.toLowerCase())
      );
      setFilteredItems(filtered);
    });
  }, [items, debouncedQuery]);

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    // ✅ 立即更新输入框
    setQuery(e.target.value);
  };

  return (
    <div>
      <input value={query} onChange={handleChange} />
      {isPending && <Spinner />}
      <Results items={filteredItems} />
    </div>
  );
}

⚠️ 使用建议

  • 先确保正确,再考虑性能: 不要一开始就用 useTransition, 先实现功能,如果发现卡顿再优化
  • 测量性能: 使用 React DevTools Profiler 测量渲染时间, 确认瓶颈确实是由渲染导致的
  • 渐进式优化: 一次只优化一个部分, 观察 effect,避免过度优化
  • 设备测试: 在不同性能的设备上测试, 确保在低端设备上也有改善

📊 useTransition vs useDeferredValue

React 提供了两个类似的并发特性工具,使用场景略有不同:

场景useTransitionuseDeferredValue
主要用途标记状态更新为低优先级延迟某个值的渲染
控制粒度控制一组状态更新控制单个值的渲染时机
适用场景搜索、Tab 切换、过滤等用户触发的更新从 props 派生的复杂 UI、需要延迟渲染的部分
典型代码startTransition(() => setState(...))const deferred = useDeferredValue(value)

推荐: 大多数情况下,如果要在用户触发的回调中包装 setState, 使用 useTransition。如果要延迟渲染从 props/state 派生的值, 使用 useDeferredValue。两者可以结合使用。

延伸阅读