useTransition 并发
本教程共 50 篇 · 第 39 篇 · 更新于 2026-07-29 · 约 9 分钟阅读
39. useTransition 并发
本节目标:学会用 useTransition 把非紧急的状态更新降级,让 UI 在繁重操作中保持响应。
问题:紧急更新被卡住
React 默认把所有状态更新当作紧急的。用户点击切换标签页,如果新标签内容要渲染 500 个组件,这 500 个会一口气渲染完,期间输入框会被卡住。
function TabContainer() {
const [tab, setTab] = useState('about');
return (
<>
<button onClick={() => setTab('about')}>关于</button>
<button onClick={() => setTab('posts')}>文章(慢)</button>
<button onClick={() => setTab('contact')}>联系</button>
{tab === 'about' && <AboutTab />}
{tab === 'posts' && <PostsTab />} {/* 假设有 500 条数据 */}
{tab === 'contact' && <ContactTab />}
</>
);
}
点”文章”按钮,PostsTab 开始渲染。如果 PostsTab 很慢(比如每条数据都要复杂计算),整个页面就冻住了。用户想改主意点”联系”都点不了。
useTransition:把更新标记成”可中断”
useTransition 返回两样东西:isPending(是否正在等待)和 startTransition(标记非紧急更新)。
import { useTransition } from 'react';
function TabContainer() {
const [tab, setTab] = useState('about');
const [isPending, startTransition] = useTransition();
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
return (
<>
<button onClick={() => selectTab('about')}>关于</button>
<button onClick={() => selectTab('posts')}>文章(慢)</button>
<button onClick={() => selectTab('contact')}>联系</button>
{tab === 'about' && <AboutTab />}
{tab === 'posts' && <PostsTab />}
{tab === 'contact' && <ContactTab />}
</>
);
}
把 setTab 放进 startTransition,React 就知道这个更新不紧急。如果用户正在 PostsTab 渲染过程中又点了”联系”,React 会中断 PostsTab 的渲染,先去处理”联系”这个紧急操作。
Note
startTransition里的函数必须同步执行。如果是异步的(比如 await 之后的 setState),需要再包一层startTransition。
显示 pending 状态
isPending 告诉你是否有 Transition 正在进行。可以用来给用户反馈:
function TabButton({ action, children, isActive }) {
const [isPending, startTransition] = useTransition();
if (isActive) return <b>{children}</b>;
if (isPending) return <b style={{ color: '#999' }}>{children}</b>;
return (
<button onClick={() => {
startTransition(async () => {
await action();
});
}}>
{children}
</button>
);
}
点击”文章”按钮后,按钮变灰色(pending 状态),PostsTab 渲染完成后恢复正常。
Actions:React 19 的写法
React 19 把 startTransition 里的函数叫做 Action。Action 可以包含异步操作:
function CheckoutForm() {
const [isPending, startTransition] = useTransition();
const [quantity, setQuantity] = useState(1);
function updateQuantity(newQty) {
startTransition(async () => {
const saved = await updateQuantityOnServer(newQty);
startTransition(() => {
setQuantity(saved); // 需要再包一层
});
});
}
return (
<div>
<input
type="number"
defaultValue={1}
onChange={e => updateQuantity(Number(e.target.value))}
/>
{isPending && <span>更新中……</span>}
<p>数量:{quantity}</p>
</div>
);
}
注意 await 之后的 setQuantity 还要再包一层 startTransition。这是当前的限制,未来可能会改进。
Warning
startTransition里的函数是立即执行的,不是延迟执行。它只是告诉 React”这里面发生的 state 更新是不紧急的”。
哪些场景适合 useTransition
- 标签页切换:新内容渲染慢,允许用户中途改主意
- 搜索筛选:筛选结果渲染慢,输入框不能卡
- 路由导航:新页面数据量大,导航可以被打断
- 大型列表渲染:渲染几千条数据时不阻塞交互
不适合的场景:
- 受控输入框:输入框的值必须同步更新,不能降级
- 简单的 state 更新:本来就很快,不需要降级
- 动画相关:需要精确控制时序,不适合并发
// ❌ 错误:不能用于受控输入
function SearchInput() {
const [text, setText] = useState('');
const [isPending, startTransition] = useTransition();
return (
<input
value={text}
onChange={e => {
startTransition(() => {
setText(e.target.value); // 输入会延迟
});
}}
/>
);
}
useTransition 与 Suspense 的配合
useTransition 配合 Suspense 可以实现一个很酷的效果:不显示 loading fallback,而是保持旧内容直到新内容就绪。
function App() {
const [tab, setTab] = useState('about');
const [isPending, startTransition] = useTransition();
return (
<Suspense fallback={<p>加载中……</p>}>
<button onClick={() => startTransition(() => setTab('about'))}>
关于
</button>
<button onClick={() => startTransition(() => setTab('posts'))}>
文章
</button>
{tab === 'about' && <AboutTab />}
{tab === 'posts' && <PostsTab />}
</Suspense>
);
}
点”文章”后,页面不会变成”加载中……”。用户看到的是旧的内容(关于页面),PostsTab 在后台加载。等新内容准备好了,React 才切换过去。
如果 PostsTab 加载很快(< 500ms),用户完全感觉不到延迟。如果加载慢,会先显示旧内容,避免白屏或闪烁。
深入理解并发渲染
React 19 的并发渲染允许 React 同时准备多个版本的 UI:
- 用户点”文章” → React 开始在后台渲染 PostsTab
- 用户又点”关于” → React 中断 PostsTab,渲染 AboutTab
- PostsTab 的渲染被丢弃,不会显示给用户
这个能力来自 React 的”双缓冲”机制。React 可以在内存里构建新的渲染树,不打扰当前显示的内容。构建完成后,一次性提交到 DOM。
Tip并发渲染不是多线程。JavaScript 还是单线程的。React 是利用调度和优先级,在浏览器空闲时做渲染工作,遇到紧急事件时让出控制权。