React useDeferredValue
从一个实际场景开始
想象你在实现一个搜索功能。用户在输入框中输入关键词,页面上有一个大列表显示匹配结果。这个列表可能有 thousands of items,每次用户输入都要重新过滤和渲染。
如果用户的电脑不够快,输入可能会感到卡顿——因为每次击键都会触发大量的列表重渲染。
你可能会想到用 useTransition 来解决这个问题:
function Search({ items }) {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
function handleInput(e) {
setQuery(e.target.value);
startTransition(() => {
// 过滤逻辑在 transition 中
setFilteredItems(filter(items, e.target.value));
});
}
}
这个方案有效。但它有一个限制:你必须在「设置 query」和「设置 filteredItems」的地方都写代码。如果 filteredItems 的计算逻辑分散在多个组件里,或者数据是从父组件传下来的,这种方式就不太方便了。
useDeferredValue 提供了一种更简洁的方式:不需要修改状态更新的位置,只需要在你需要「延迟使用」的值外面包一层 useDeferredValue。
useDeferredValue 的基本用法
useDeferredValue 接受一个值作为参数,返回这个值的「延迟版本」:
const deferredQuery = useDeferredValue(query);
当 query 变化时,deferredQuery 可能暂时和 query 不同步——React 会优先保证 query 的更新,让 deferredQuery 稍后追上。
完整示例
import { useState, useDeferredValue } from 'react';
function Search({ items }) {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const filteredItems = useMemo(() => {
return items.filter(item =>
item.name.toLowerCase().includes(deferredQuery.toLowerCase())
);
}, [items, deferredQuery]);
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} />
<div style={{ opacity: query !== deferredQuery ? 0.5 : 1 }}>
{filteredItems.map(item => (
<SearchResult key={item.id} item={item} />
))}
</div>
</div>
);
}
在这个例子里:
- 用户输入更新
query(紧急更新,立即响应) filteredItems的计算基于deferredQuery(非紧急更新,可能延迟)- 如果
query和deferredQuery不同步,说明 transition 还在进行中,可以显示加载态
useDeferredValue 与 useTransition:核心区别
这两个 API 都涉及「延迟」,但解决的问题不同:
| useTransition | useDeferredValue | |
|---|---|---|
| 作用对象 | 包装状态更新 | 包装已存在的值 |
| 使用方式 | startTransition(() => setState()) |
const deferred = useDeferredValue(state) |
| 典型场景 | 状态更新逻辑在当前组件 | 值来自 props 或复杂的计算结果 |
| 是否需要修改状态逻辑 | 需要把 setState 包在 startTransition 里 | 不需要,只需用 useDeferredValue 包装 |
什么时候用 useTransition
当你在同一个组件内有两个相关的状态更新,而你想让其中一个延迟:
function Search() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
function handleChange(e) {
setQuery(e.target.value); // 紧急
startTransition(() => {
setResults(filter(e.target.value)); // 非紧急
});
}
}
什么时候用 useDeferredValue
当你从外部接收一个值,而这个值的消费者应该被延迟:
function SearchResults({ query, items }) {
// query 从父组件传来,无法修改它的 setState
// 用 useDeferredValue 延迟对 query 的使用
const deferredQuery = useDeferredValue(query);
const filteredItems = useMemo(() => {
return items.filter(item =>
item.name.toLowerCase().includes(deferredQuery.toLowerCase())
);
}, [items, deferredQuery]);
return <List items={filteredItems} />;
}
一个形象的比喻
想象你在厨房做菜:
- useTransition 像是告诉你的助手:「先洗菜(紧急),等洗完了再切菜(非紧急)」
- useDeferredValue 像是给你一个「延迟版本」的菜谱——你看到的是当前菜谱,但助手可以稍后才参考这个菜谱去执行
典型场景:输入框 + 过滤大列表
useDeferredValue 最典型的场景是:用户输入变化很快,但结果的渲染可以稍微延迟。
考虑一个通讯录应用,列表中有 thousands of contacts:
function ContactList({ contacts }) {
const [searchTerm, setSearchTerm] = useState('');
const deferredSearchTerm = useDeferredValue(searchTerm);
const filteredContacts = useMemo(() => {
// 基于 deferred 的值过滤,渲染可以更流畅
return contacts.filter(contact =>
contact.name.toLowerCase().includes(deferredSearchTerm.toLowerCase()) ||
contact.email.toLowerCase().includes(deferredSearchTerm.toLowerCase())
);
}, [contacts, deferredSearchTerm]);
const isStale = searchTerm !== deferredSearchTerm;
return (
<div>
<input
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
placeholder="Search contacts..."
/>
<div style={{ opacity: isStale ? 0.6 : 1 }}>
{filteredContacts.map(contact => (
<ContactCard key={contact.id} contact={contact} />
))}
</div>
</div>
);
}
这个模式的优势:
- 输入本身是流畅的,每次击键都立即响应
- 列表的过滤和渲染会被延迟处理
- 如果用户快速连续输入,过滤会「跟不上」,但这没关系——用户看到的是旧结果(而不是卡顿的输入)
为什么 deferredValue 和原始值可能不同步
useDeferredValue 返回的延迟值和原始值可能暂时不同步。这是设计如此,不是 bug。
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
// 初始状态:query = '', deferredQuery = ''
// 用户输入 'a':query = 'a', deferredQuery 可能还是 ''
// React 在处理完紧急更新后,才会更新 deferredQuery
// 如果用户继续输入 'ab',query = 'ab', deferredQuery 可能还是 '' 或刚变成 'a'
React 这样做是为了优先保证紧急更新的响应性。如果每次 query 变化都要同步更新 deferredQuery,那就失去了「延迟」的意义。
使用时注意:不要把应该同步的逻辑延迟了
useDeferredValue 不是万能的。如果两个值之间有依赖关系,延迟其中一个可能导致问题:
错误示例:deferred 值用在应该同步的地方
function ProductPage({ productId }) {
const [selectedVariant, setSelectedVariant] = useState(null);
const deferredVariant = useDeferredValue(selectedVariant);
// 错误:价格显示用了 deferred 的值
// 如果用户切换 variant,价格可能显示旧的值
return (
<div>
<ProductDetails productId={productId} variant={deferredVariant} />
<Price variant={deferredVariant} /> // 可能显示旧价格!
<AddToCartButton variant={deferredVariant} /> // 可能用旧 variant
</div>
);
}
正确示例:只在渲染列表时用 deferred 值
function ProductPage({ productId }) {
const [selectedVariant, setSelectedVariant] = useState(null);
const deferredVariant = useDeferredValue(selectedVariant);
return (
<div>
<ProductDetails productId={productId} variant={selectedVariant} />
{/* 价格的计算和显示是同步的 */}
<Price variant={selectedVariant} />
{/* 评论区可能有大量评论,用 deferred 版本延迟渲染 */}
<CommentSection productId={productId} variant={deferredVariant} />
<AddToCartButton variant={selectedVariant} />
</div>
);
}
工程判断原则
用 useDeferredValue 时,问自己一个问题:这个值的「旧」版本会不会导致错误的用户界面?
- 如果答案是「不会」——比如搜索结果列表显示旧结果没问题——那么可以用 deferred value
- 如果答案是「会」——比如价格显示错误、按钮功能错误——那么不应该用 deferred value
Suspense 的结合:deferredValue 和 Suspense 配合
useDeferredValue 和 Suspense 配合可以创造更流畅的用户体验:
function SearchResults({ query }) {
const deferredQuery = useDeferredValue(query);
return (
<Suspense fallback={<LoadingSkeleton />}>
<SearchResultsContent query={deferredQuery} />
</Suspense>
);
}
这里的逻辑是:
- 如果
deferredQuery比query落后很多,React 认为这是一个 "delayed" 的 render - Suspense 会等待一段时间,看看 deferredQuery 能不能追上
- 如果 deferredQuery 追上来了,直接显示内容(没有 loading 闪烁)
- 如果 deferredQuery 迟迟追不上,Suspense 显示 fallback
这种模式和传统的「显示 loading 等数据」不同——它创造了一种「先显示旧内容,等新内容准备好了再更新」的感觉。
useDeferredValue 的内部机制
理解 useDeferredValue 的实现有助于更好地使用它。
useDeferredValue 本质上是React 的调度机制的一种应用:
- 当
query变化时,React 立即开始处理这个更新(紧急) - React 同时调度一个「延迟更新」来更新
deferredQuery - 在渲染时,React 使用
deferredQuery来渲染列表 - 如果期间有新的紧急更新(用户继续输入),React 会优先处理
deferredQuery的更新可能被多次「跳过」,直到最终追上query
这意味着 deferredQuery 的更新是**「best effort」**的——React 会尽力让它追上原始值,但如果系统繁忙,它可能会落后更多。这不是 bug,是 React 在资源受限时的合理降级策略。
和 useTransition 的关系
在 React 内部,useDeferredValue 的实现可能使用或类似于 useTransition 的机制。实际上,如果你看 React 18 的实现,useDeferredValue 内部就是用类似 useTransition 的方式来延迟状态传播的。
但两者面向的抽象层次不同:
useTransition让你控制「哪些 setState 是紧急的,哪些不是」useDeferredValue让你控制「这个值的消费者应该被延迟」
常见误区
误区一:deferred value 总是落后于原始值
实际上,deferredQuery 在大多数情况下会很快追上 query。只有在连续快速输入时,它才会明显落后。如果用户停顿一下输入,deferredQuery 会很快变成和 query 一样。
误区二:可以用 useDeferredValue 来「缓存」计算结果
useDeferredValue 不是缓存工具。它不存储计算结果,只是延迟值的传播。如果你想要缓存,用 useMemo 或 useCallback。
误区三:所有列表渲染都应该用 useDeferredValue
过度使用 useDeferredValue 可能会导致 UI 看起来「慢半拍」。只有在确实有性能问题(列表很大、渲染很慢)时才需要用。
面试中的表达
面试官问 useDeferredValue,通常想确认你理解它和 useTransition 的区别:
useDeferredValue 和 useTransition 都涉及「延迟」,但解决的问题不同。useTransition 让你把一个状态更新标记为非紧急的,用在「状态更新逻辑在同一个组件里」的场景。useDeferredValue 让你延迟「值的使用」,用在「值从外部传来,无法修改状态更新逻辑」的场景。
它们的本质区别是:useTransition 控制的是「生产端」(setState 被标记为非紧急),useDeferredValue 控制的是「消费端」(值的使用被延迟)。useDeferredValue 不改变状态更新的时机,只改变状态值「传播到消费者」的时机。
工程判断的关键是:deferred 版本的值如果显示旧内容,用户能接受吗?如果能,用 useDeferredValue;如果不能,就不要用。
延展阅读
- React Docs: useDeferredValue — 官方 useDeferredValue 文档
- React Blog: React 18 Release — React 18 并发特性发布说明
- Deep Dive: React 18 Suspense + Transitions — Suspense 和 transition 的深入讨论
- Dan Abramov: Working with React — 搜索 useDeferredValue 相关文章