React 18 推出两个神奇 Hook:
useTransition和useDeferredValue。它们像拥有"时间魔法"的小助手,让你的页面在大数据渲染时依然顺滑。今天我们用动画故事讲清楚:什么时候用?怎么用?到底解决了什么"卡顿"?
🎬 场景故事:搜索框输入就卡住?
想象你在做一个商品搜索页:
- 输入框每输入一个字就要请求接口 + 渲染商品列表。
- 商品列表有上千条,每次渲染都要 200ms。
- 用户一输入,页面就像慢动作,体验极差。
React 18 的新 Hook 就是为了解决"紧急任务(输入反馈)"和"非紧急任务(渲染大列表)"之间的冲突。
⚡ useTransition:给任务排优先级
jsx
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
const value = e.target.value;
setInputValue(value); // 紧急任务:更新输入框
startTransition(() => {
setFilteredList(filterBigList(value)); // 不紧急:渲染大列表
});
};
setInputValue立即执行,让输入框不掉帧。setFilteredList放到startTransition里,被标记为"低优先级",React 会在空闲时渲染。isPending是状态,告诉我们低优任务还在准备中,可以配个加载中提示。
小比喻:
- 没有
useTransition:所有任务都排队,用户输入也要等大列表渲染完。 - 使用
useTransition:就像 VIP 通道,输入先获得响应,大列表慢慢渲染。
🧪 完整示例:
jsx
import { useState, useMemo, useTransition } from 'react';
function HugeList({ query }) {
const list = useMemo(() => {
const data = [];
for (let i = 0; i < 10000; i++) {
data.push(`${query} 商品 ${i}`);
}
return data;
}, [query]);
return (
<ul>
{list.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
);
}
export default function SearchPage() {
const [value, setValue] = useState('');
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
const nextValue = e.target.value;
setValue(nextValue);
startTransition(() => {
setQuery(nextValue);
});
};
return (
<div>
<input value={value} onChange={handleChange} />
{isPending && <p>列表加载中...</p>}
<HugeList query={query} />
</div>
);
}
🧠 useDeferredValue:给"结果值"降降速
如果你没法控制触发更新的位置,比如 query 是父组件传来的 prop,这时候 useTransition 不好用,换 useDeferredValue。
jsx
const deferredQuery = useDeferredValue(query);
const list = useMemo(() => filterBigList(deferredQuery), [deferredQuery]);
- 读取
deferredQuery会比真实的query慢一点点(React 自动延迟)。 - 当
query快速变化时,组件优先渲染旧的数据,待空闲了再更新为最新值。
小贴士:
useDeferredValue就像看视频时开"倍速补偿",保证画面不断顿。- 它还会帮助自动跳过中间的过时值,比如你飞快输入"iphone",最终只渲染最新的。
⚖️ useTransition vs useDeferredValue
| 对比 | useTransition | useDeferredValue |
|---|---|---|
| 使用位置 | 触发更新的地方 | 消费值的地方 |
| 作用对象 | 一段状态更新逻辑 | 一个具体的值 |
| 是否返回 pending | 是 | 否(需要手动比较) |
| 常见场景 | 输入框、Tab 切换、路由跳转 | 父组件传下来的大数据列表 |
口诀:"能包住 setState 用 useTransition,拿到值想慢一点用 useDeferredValue"。
🧭 实战案例:搜索防抖 + 流畅列表
jsx
function ProductSearch() {
const [keyword, setKeyword] = useState('');
const [result, setResult] = useState([]);
const [isPending, startTransition] = useTransition();
useEffect(() => {
const timer = setTimeout(() => {
startTransition(async () => {
const data = await fetchProducts(keyword);
setResult(data);
});
}, 300);
return () => clearTimeout(timer);
}, [keyword, startTransition]);
return (
<div>
<input value={keyword} onChange={(e) => setKeyword(e.target.value)} />
{isPending && <p>聪明搜索中...</p>}
<ProductList data={result} />
</div>
);
}
- 输入框即时响应。
- 列表渲染放到并发更新里。
- 结合防抖避免不必要的请求。
🚥 什么时候不要用?
- 页面非常简单(几十个元素),不用画蛇添足。
- 依赖外部库(比如某些 UI 组件)不支持并发渲染时,需要测试是否兼容。
- 别把
startTransition包得太细,否则代码反而不好读。
🧱 和 Vue 的对比
- Vue 3 有
suspense+transition,但优先级调度主要靠内部实现,开发者控制不多。 - React 把控制权交给开发者,让我们明确哪些是"紧急"和"慢速"任务。
- 可以写一篇"React useTransition vs Vue Suspense 实战对比",引流效果拔群。
✅ 总结
useTransition:包裹setState,给予低优先级,返回isPending控制加载状态。useDeferredValue:返回延迟更新的值,避免组件一次次渲染。- 适合处理大列表渲染、复杂计算、慢接口响应。
- 搭配
Suspense、lazy效果更佳,能实现"骨架屏 + 并发渲染"。
🏁 小练习
- 把你项目里最卡的列表试着套上
useTransition,对比 FPS。 - 写一个实时搜索页面,比较"普通 setState"和"useDeferredValue"滑顺程度的差异。
- 写篇博客:
React 18 实战:让搜索框从 5 FPS 到 60 FPS 的秘密。