React Hook useTransition
一、核心定位:解决 “优先级错配” 的渲染优化 Hook
useDeferredValue 是 React 提供的并发渲染优化工具,核心作用是生成一个 “延迟版的状态值”,让非紧急 UI 更新(如长列表)延后执行,优先保证用户直接交互(如输入、点击)的实时响应,从而解决 “大量节点渲染阻塞交互” 的卡顿问题。
它的本质是:分离状态更新优先级,把 “用户感知强的紧急更新” 和 “耗时的非紧急更新” 错开,避免 JS 引擎被占用导致的卡顿。
memo + deferredValue 的组合,本质是让长列表组件只 “响应延迟状态的变化”,而不响应输入框的实时变化
二、基础用法与关键特性
1. 基础语法
import { useDeferredValue, useState } from "react";
function App() {
// 1. 声明紧急状态(如输入框值)
const [value, setValue] = useState("");
// 2. 生成延迟状态(非紧急 UI 用这个值)
const deferredValue = useDeferredValue(value);
return (
<div>
{/* 紧急 UI:用原始状态,实时响应 */}
<input value={value} onChange={(e) => setValue(e.target.value)} />
{/* 非紧急 UI:用延迟状态,延后更新 */}
<LongList data={deferredValue} />
</div>
);
}
2. 两个关键特性
- 自动合并更新:用户快速操作(如连续输入
1→2→3)时,deferredValue会跳过中间值,直接更新为最终值(如123),避免非紧急 UI 反复渲染。 - 不阻塞紧急更新:
deferredValue的更新会被标记为 “低优先级”,只有当浏览器处理完紧急更新(如输入框显示)并空闲时,才会执行非紧急 UI 的更新。
三、必须配合的 “生效条件”
useDeferredValue 单独使用时效果有限,必须配合以下两个条件才能完全发挥作用,否则会被 “无效协调” 抵消:
1. 非紧急 UI 需独立抽离为组件
将长列表等耗时 UI 从主组件(如 App)中抽离为独立组件(如 LongList),避免 “紧急更新触发整棵组件树的全量协调”。反例:若长列表与输入框在同一组件,即使使用 deferredValue,React 仍会遍历所有节点,导致无效协调。
2. 配合 memo 阻止无效重渲染
用 memo 包裹独立的非紧急组件,让组件仅在延迟状态(deferredValue)变化时才重渲染,避免 “父组件更新导致非紧急组件反复协调”。
// 1. 抽离长列表组件,并用 memo 包裹
const LongList = memo((props: { data: string }) => {
return (
<div className="list">
{/* 50000 个节点:仅在 props.data(deferredValue)变化时重渲染 */}
{Array(50000)
.fill("")
.map((_, idx) => (
<div key={idx}>{props.data}</div>
))}
</div>
);
});
// 2. 主组件中使用延迟状态
function App() {
const [value, setValue] = useState("");
const deferredValue = useDeferredValue(value); // 延迟状态
return (
<div>
<input value={value} onChange={(e) => setValue(e.target.value)} />
<LongList data={deferredValue} /> {/* 非紧急 UI 用延迟状态 */}
</div>
);
}
四、核心原理:为什么能解决卡顿?
卡顿的根源是 “大量节点的协调(reconcile)过程占用 JS 引擎”,useDeferredValue 通过以下两步从根本上优化:
1. 第一步:延迟非紧急 UI 的协调时机
- 紧急更新(输入框):
value变化时,输入框立即协调(仅 1 个节点,速度快),优先响应用户输入; - 非紧急更新(长列表):
deferredValue延迟更新,长列表的协调过程被延后到浏览器空闲时执行,不阻塞输入。
2. 第二步:合并中间更新减少协调次数
用户快速输入时,deferredValue 不跟随每次 value 变化,而是合并为最终值后一次性更新,让长列表只协调 1 次,而非多次反复协调(如输入 1→2→3,只在 123 时协调 1 次)。
五、常见误区与注意事项
1. 认为 “用了就生效”,不抽离组件 / 不用 memo
- 问题:若长列表与输入框在同一组件,或未用
memo,React 仍会频繁协调大量节点,deferredValue被无效协调抵消; - 解决:必须 “组件抽离 + memo 缓存”,精准控制非紧急 UI 的更新时机。
memo缓存的子组件不会因为父组件更新而重新渲染,因而依赖props为defferedValue的长列表组件不会跟随进行协调。
2. 延迟状态会导致 “视觉闪烁”
- 实际:
deferredValue会保留旧值直到更新,非紧急 UI 不会空白或闪烁,只是更新滞后,用户感知为 “输入实时,列表慢一点”,属于可接受的体验。
3. 注意:只优化 “渲染卡顿”,不优化 “数据计算”
useDeferredValue 仅优化 “UI 协调与渲染” 的优先级,若卡顿源于 “大量数据计算”(如复杂排序),需配合 useMemo 优化计算结果,二者分工不同。
六、适用场景与对比
1. 核心适用场景
- 输入框 + 长列表(如搜索框实时联想);
- 表单输入 + 实时图表渲染;
- 任何 “用户交互需实时响应,同时伴随大量节点渲染” 的场景。
2. 与 useTransition 的区别
| 特性 | useDeferredValue | useTransition |
|---|---|---|
| 核心作用 | 生成延迟状态值,控制 UI 依赖 | 标记状态更新为低优先级,批量延迟 |
| 适用场景 | 基于状态值的 UI 优先级分离,接受父组件的传递的值得延迟更新 | 批量低优先级更新(如按钮触发列表刷新,用户交互触发得状态更新) |
| 依赖配合 | 需配合 memo + 组件抽离 | 可单独使用,无需额外依赖 |
| 主动被动 | 被动延迟值的更新 | 主动标记低优先级的更新 |
七、手写模拟useDefferedValue实现
export default function App() {
const [value, setValue] = useState("");
const [isPending, startTransition] = useTransition();
const [deferredValue, setDeferredValue] = useState("");
useEffect(() => {
startTransition(() => setDeferredValue(value));
}, [value, startTransition]);
const handleChange = (e) => {
setValue(e.target.value);
};
return (
<div>
<input value={value} onChange={handleChange} />
<LongList value={deferredValue} />
</div>
);
}
const LongList = memo((props) => {
useLayoutEffect(() => {
// 浏览器渲染前移除大量的 dom 节点,排除浏览器渲染大量节点的影响
var container = document.getElementsByClassName("container");
var list = document.getElementsByClassName("list");
if (list.length) {
container[0].removeChild(list[0]);
}
});
return (
<div className="container">
{Array(100)
.fill("a")
.map((item) => (
<div>{props.value}</div>
))}
<div className="list">
{Array(50000)
.fill("a")
.map((item) => (
<div>{props.value}</div>
))}
</div>
</div>
);
});