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
]返回值
| 返回值 | 类型 | 说明 |
|---|---|---|
isPending | boolean | 是否有 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: 搜索输入框(基础用法)
最常见的场景:用户输入立即显示,搜索结果延迟计算。
import { useState, useTransition } from 'react';
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);
// ⏳ 非紧急更新: 搜索结果可以延迟(用户可以接受短暂等待)
startTransition(() => {
const filtered = items.filter(item =>
item.toLowerCase().includes(value.toLowerCase())
);
setFilteredItems(filtered);
});
};
return (
<div>
<input
type="text"
value={query}
onChange={handleChange}
placeholder="搜索..."
className="border p-2 rounded"
/>
{isPending && (
<p className="text-sm text-gray-500 mt-2">搜索中...</p>
)}
<ul className="mt-4 space-y-1">
{filteredItems.map(item => (
<li key={item} className="text-sm">{item}</li>
))}
</ul>
</div>
);
}效果: 用户打字时,输入框立即响应,即使 items 数组很大(如 10,000 条), 也不会感到卡顿。搜索结果会在浏览器空闲时更新。
示例 2: Tab 切换(生产级,结合 Suspense)
Tab 切换是另一个典型场景:点击立即响应,内容延迟加载。
import { useState, useTransition, Suspense } from 'react';
// 模拟异步数据获取
async function fetchTabData(tabName: string): Promise<string[]> {
// 假设这是一个 API 调用
await new Promise(resolve => setTimeout(resolve, 1000));
return [`${tabName} 数据 1`, `${tabName} 数据 2`, `${tabName} 数据 3`];
}
// Tab 内容组件(使用 Suspense)
function TabContent({ tabName }: { tabName: string }) {
// 抛出 Promise 触发 Suspense
const data = fetchTabData(tabName);
throw data;
// 实际项目中会用 use() Hook 或数据获取库
// return <div>{data.map(...)}</div>;
}
function TabContainer() {
const [activeTab, setActiveTab] = useState<'home' | 'posts' | 'about'>('home');
const [isPending, startTransition] = useTransition();
const switchTab = (tab: 'home' | 'posts' | 'about') => {
startTransition(() => {
// ⏳ Tab 切换标记为 transition,内容加载不会阻塞点击反馈
setActiveTab(tab);
});
};
return (
<div>
{/* Tab 按钮 */}
<div className="flex gap-2 mb-4">
{(['home', 'posts', 'about'] as const).map(tab => (
<button
key={tab}
onClick={() => switchTab(tab)}
className={`px-4 py-2 rounded ${
activeTab === tab
? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-700'
}`}
>
{tab.charAt(0).toUpperCase() + tab.slice(1)}
{isPending && activeTab === tab && (
<span className="ml-2">加载中...</span>
)}
</button>
))}
</div>
{/* Tab 内容(带 Suspense) */}
<Suspense fallback={<div className="p-4">加载中...</div>}>
{activeTab === 'home' && <div>首页内容</div>}
{activeTab === 'posts' && <div>文章内容</div>}
{activeTab === 'about' && <div>关于内容</div>}
</Suspense>
</div>
);
}效果: 点击 Tab 按钮时,按钮样式立即变化(用户得到反馈), 内容区域显示加载状态,数据加载完成后显示内容。整个过程流畅,不会"卡住"界面。
示例 3: 树形列表展开(复杂场景)
树形结构展开时,可能需要渲染大量子节点。使用 transition 可以保持交互流畅。
import { useState, useTransition } from 'react';
interface TreeNode {
id: string;
name: string;
children?: TreeNode[];
}
function TreeView({ data }: { data: TreeNode[] }) {
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
const [isPending, startTransition] = useTransition();
const toggleNode = (nodeId: string) => {
// ✅ 立即更新展开/折叠状态(视觉反馈)
const nextExpandedIds = new Set(expandedIds);
if (nextExpandedIds.has(nodeId)) {
nextExpandedIds.delete(nodeId);
} else {
nextExpandedIds.add(nodeId);
}
// ⏳ 延迟渲染子节点(可能很大量)
startTransition(() => {
setExpandedIds(nextExpandedIds);
});
};
return (
<ul className="space-y-1">
{data.map(node => (
<TreeNode
key={node.id}
node={node}
expandedIds={expandedIds}
onToggle={toggleNode}
isPending={isPending}
/>
))}
</ul>
);
}
function TreeNode({
node,
expandedIds,
onToggle,
isPending
}: {
node: TreeNode;
expandedIds: Set<string>;
onToggle: (id: string) => void;
isPending: boolean;
}) {
const isExpanded = expandedIds.has(node.id);
const hasChildren = node.children && node.children.length > 0;
return (
<li>
<div
onClick={() => onToggle(node.id)}
className="flex items-center gap-2 cursor-pointer hover:bg-gray-100 p-1 rounded"
>
{hasChildren && (
<span className="text-gray-500">
{isExpanded ? '▼' : '▶'}
</span>
)}
<span>{node.name}</span>
{isPending && isExpanded && (
<span className="text-xs text-gray-400">展开中...</span>
)}
</div>
{isExpanded && hasChildren && (
<ul className="ml-6 mt-1 space-y-1">
{node.children!.map(child => (
<TreeNode
key={child.id}
node={child}
expandedIds={expandedIds}
onToggle={onToggle}
isPending={isPending}
/>
))}
</ul>
)}
</li>
);
}
// 使用示例
const treeData: TreeNode[] = [
{
id: '1',
name: '根节点',
children: [
{
id: '1-1',
name: '子节点 1',
children: Array.from({ length: 100 }, (_, i) => ({
id: `1-1-${i}`,
name: `深层节点 ${i}`
}))
},
{
id: '1-2',
name: '子节点 2',
children: Array.from({ length: 100 }, (_, i) => ({
id: `1-2-${i}`,
name: `深层节点 ${i}`
}))
}
]
}
];
// <TreeView data={treeData} />效果: 点击展开图标时,图标立即旋转(用户得到反馈), 子节点在浏览器空闲时渲染。即使有数千个子节点,也不会阻塞界面。
示例 4: 数据可视化仪表板(生产级)
在数据可视化中,过滤器和图表都很"重",使用 transition 分离紧急和非紧急更新。
import { useState, useTransition, useMemo } from 'react';
interface DataPoint {
id: string;
category: string;
value: number;
timestamp: number;
}
interface Filters {
category?: string;
dateRange?: [number, number];
minValue?: number;
}
function Dashboard({ rawData }: { rawData: DataPoint[] }) {
const [filters, setFilters] = useState<Filters>({});
const [filteredData, setFilteredData] = useState<DataPoint[]>(rawData);
const [isPending, startTransition] = useTransition();
// 应用过滤器(可能在大量数据上执行复杂计算)
const applyFilters = (newFilters: Filters) => {
let result = rawData;
if (newFilters.category) {
result = result.filter(d => d.category === newFilters.category);
}
if (newFilters.dateRange) {
const [start, end] = newFilters.dateRange;
result = result.filter(d => d.timestamp >= start && d.timestamp <= end);
}
if (newFilters.minValue !== undefined) {
result = result.filter(d => d.value >= newFilters.minValue!);
}
return result;
};
const handleFilterChange = (key: keyof Filters, value: any) => {
const newFilters = { ...filters, [key]: value };
// ✅ 立即更新过滤器 UI(选中状态、下拉框显示等)
setFilters(newFilters);
// ⏳ 延迟数据过滤和图表重绘(可能很耗时)
startTransition(() => {
const filtered = applyFilters(newFilters);
setFilteredData(filtered);
});
};
// 计算统计数据(也放在 transition 中)
const stats = useMemo(() => {
const total = filteredData.length;
const sum = filteredData.reduce((acc, d) => acc + d.value, 0);
const avg = total > 0 ? sum / total : 0;
return { total, sum, avg };
}, [filteredData]);
return (
<div className="p-4">
{/* 过滤器面板 */}
<div className="mb-6 p-4 bg-gray-50 rounded">
<h3 className="font-semibold mb-3">过滤器</h3>
<div className="space-y-3">
<div>
<label className="block text-sm mb-1">类别</label>
<select
value={filters.category || ''}
onChange={(e) => handleFilterChange('category', e.target.value || undefined)}
className="w-full border p-2 rounded"
>
<option value="">全部</option>
<option value="A">类别 A</option>
<option value="B">类别 B</option>
<option value="C">类别 C</option>
</select>
</div>
<div>
<label className="block text-sm mb-1">最小值</label>
<input
type="number"
value={filters.minValue || ''}
onChange={(e) => handleFilterChange('minValue', Number(e.target.value) || undefined)}
className="w-full border p-2 rounded"
placeholder="输入最小值..."
/>
</div>
</div>
</div>
{/* 加载状态 */}
{isPending && (
<div className="mb-4 p-2 bg-yellow-50 text-yellow-700 text-sm rounded">
更新图表中...
</div>
)}
{/* 统计数据 */}
<div className="mb-6 p-4 bg-blue-50 rounded grid grid-cols-3 gap-4">
<div>
<div className="text-sm text-gray-600">总记录数</div>
<div className="text-2xl font-bold">{stats.total}</div>
</div>
<div>
<div className="text-sm text-gray-600">总和</div>
<div className="text-2xl font-bold">{stats.sum.toFixed(2)}</div>
</div>
<div>
<div className="text-sm text-gray-600">平均值</div>
<div className="text-2xl font-bold">{stats.avg.toFixed(2)}</div>
</div>
</div>
{/* 图表区域 */}
<div className="p-4 bg-white border rounded">
<h3 className="font-semibold mb-3">数据可视化</h3>
<div className="h-64 flex items-center justify-center text-gray-400">
{/* 实际项目中这里会是图表库(如 Recharts、Chart.js 等) */}
图表区域 ({filteredData.length} 条数据)
</div>
</div>
</div>
);
}效果: 用户调整过滤器时,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 提供了两个类似的并发特性工具,使用场景略有不同:
| 场景 | useTransition | useDeferredValue |
|---|---|---|
| 主要用途 | 标记状态更新为低优先级 | 延迟某个值的渲染 |
| 控制粒度 | 控制一组状态更新 | 控制单个值的渲染时机 |
| 适用场景 | 搜索、Tab 切换、过滤等用户触发的更新 | 从 props 派生的复杂 UI、需要延迟渲染的部分 |
| 典型代码 | startTransition(() => setState(...)) | const deferred = useDeferredValue(value) |
推荐: 大多数情况下,如果要在用户触发的回调中包装 setState, 使用 useTransition。如果要延迟渲染从 props/state 派生的值, 使用 useDeferredValue。两者可以结合使用。