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: 搜索输入框(基础用法)
最常见的场景:用户输入立即显示,搜索结果延迟计算。
效果: 用户打字时,输入框立即响应,即使 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 提供了两个类似的并发特性工具,使用场景略有不同:
| 场景 | useTransition | useDeferredValue |
|---|---|---|
| 主要用途 | 标记状态更新为低优先级 | 延迟某个值的渲染 |
| 控制粒度 | 控制一组状态更新 | 控制单个值的渲染时机 |
| 适用场景 | 搜索、Tab 切换、过滤等用户触发的更新 | 从 props 派生的复杂 UI、需要延迟渲染的部分 |
| 典型代码 | startTransition(() => setState(...)) | const deferred = useDeferredValue(value) |
推荐: 大多数情况下,如果要在用户触发的回调中包装 setState, 使用 useTransition。如果要延迟渲染从 props/state 派生的值, 使用 useDeferredValue。两者可以结合使用。