useDeferredValue
是一个 React Hook,它允许您延迟更新 UI 的一部分。
const deferredValue = useDeferredValue(value)
参考
useDeferredValue(value, initialValue?)
在组件的顶层调用useDeferredValue
以获取该值的延迟版本。
import { useState, useDeferredValue } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
// ...
}
参数
value
: 您想要延迟的值。它可以是任何类型。- 可选
initialValue
: 在组件的初始渲染期间使用的值。如果省略此选项,useDeferredValue
在初始渲染期间不会延迟,因为没有它可以渲染的value
的先前版本。
返回值
currentValue
: 在初始渲染期间,返回的延迟值将是initialValue
,或者与您提供的值相同。在更新期间,React 将首先尝试使用旧值重新渲染(因此它将返回旧值),然后尝试使用新值在后台进行另一次重新渲染(因此它将返回更新后的值)。
注意事项
-
当更新位于 Transition 内部时,
useDeferredValue
始终返回新的value
并且不会产生延迟渲染,因为更新本身已经延迟了。 -
传递给
useDeferredValue
的值应该要么是原始值(例如字符串和数字),要么是在渲染之外创建的对象。如果您在渲染过程中创建了一个新对象并立即将其传递给useDeferredValue
,它在每次渲染时都会不同,从而导致不必要的后台重新渲染。 -
当
useDeferredValue
接收到一个不同的值(与Object.is
比较)时,除了当前渲染(仍然使用先前值)之外,它还会在后台安排重新渲染以使用新值。后台重新渲染是可以中断的:如果value
有其他更新,React 将从头开始重新启动后台重新渲染。例如,如果用户输入的速度快于接收其延迟值的图表重新渲染的速度,则只有在用户停止输入后,图表才会重新渲染。 -
useDeferredValue
与<Suspense>
集成。如果由新值引起的后台更新挂起了UI,用户将看不到回退内容。他们将看到旧的延迟值,直到数据加载完成。 -
useDeferredValue
本身并不会阻止额外的网络请求。 -
useDeferredValue
本身不会造成固定的延迟。一旦 React 完成原始重新渲染,React 将立即开始使用新的延迟值进行后台重新渲染。由事件(例如打字)引起的任何更新都将中断后台重新渲染并获得优先级。 -
由
useDeferredValue
引起的后台重新渲染,直到提交到屏幕上才会触发 Effects。如果后台重新渲染挂起,则其 Effects 将在数据加载并 UI 更新后运行。
用法
在加载新内容时显示旧内容
在组件的顶层调用useDeferredValue
来延迟更新UI的某些部分。
import { useState, useDeferredValue } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
// ...
}
在初始渲染期间,延迟值将与您提供的值相同。
在更新期间,延迟值将“滞后于”最新的值。特别是,React 将首先不更新延迟值就重新渲染,然后尝试在后台使用新接收的值重新渲染。
让我们来看一个例子,看看这在什么时候有用。
在此示例中,SearchResults
组件在获取搜索结果时挂起。尝试输入"a"
,等待结果,然后将其编辑为"ab"
。"a"
的结果将被加载回退内容替换。
import { Suspense, useState } from 'react'; import SearchResults from './SearchResults.js'; export default function App() { const [query, setQuery] = useState(''); return ( <> <label> Search albums: <input value={query} onChange={e => setQuery(e.target.value)} /> </label> <Suspense fallback={<h2>Loading...</h2>}> <SearchResults query={query} /> </Suspense> </> ); }
一个常见的替代UI模式是延迟更新结果列表,并在新结果准备就绪之前继续显示先前结果。调用useDeferredValue
以传递查询的延迟版本
export default function App() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
<>
<label>
Search albums:
<input value={query} onChange={e => setQuery(e.target.value)} />
</label>
<Suspense fallback={<h2>Loading...</h2>}>
<SearchResults query={deferredQuery} />
</Suspense>
</>
);
}
query
将立即更新,因此输入将显示新值。但是,deferredQuery
将保留其先前值,直到数据加载完成,因此SearchResults
将短暂显示旧的结果。
在下面的示例中输入"a"
,等待结果加载,然后将输入编辑为"ab"
。请注意,您现在看到的是旧的结果列表,而不是Suspense回退内容,直到新结果加载完毕。
import { Suspense, useState, useDeferredValue } from 'react'; import SearchResults from './SearchResults.js'; export default function App() { const [query, setQuery] = useState(''); const deferredQuery = useDeferredValue(query); return ( <> <label> Search albums: <input value={query} onChange={e => setQuery(e.target.value)} /> </label> <Suspense fallback={<h2>Loading...</h2>}> <SearchResults query={deferredQuery} /> </Suspense> </> ); }
深入探讨
您可以将其视为分两步完成
-
首先,React 使用新的
query
("ab"
) 重新渲染,但使用旧的deferredQuery
(仍然是"a"
)。deferredQuery
值传递给结果列表,它是延迟的:它“滞后于”query
值。 -
在后台,React 尝试使用更新后的
query
和deferredQuery
值 ("ab"
) 重新渲染。如果此重新渲染完成,React 将将其显示在屏幕上。但是,如果它挂起("ab"
的结果尚未加载),React 将放弃此渲染尝试,并在数据加载后再次重试此重新渲染。在数据准备就绪之前,用户将继续看到过时的延迟值。
延迟的“后台”渲染是可以中断的。例如,如果您再次在输入框中键入内容,React 将放弃它并使用新值重新启动。React 将始终使用最新提供的 value。
请注意,每次按键仍然会发出网络请求。这里延迟的是显示结果(直到结果准备就绪),而不是网络请求本身。即使用户继续键入,每次按键的响应都会被缓存,因此按 Backspace 键是即时的,不会再次获取。
指示内容已过期
在上面的例子中,没有指示最新查询的结果列表仍在加载中。如果新结果需要一段时间才能加载,这可能会让用户感到困惑。为了更清楚地向用户表明结果列表与最新查询不匹配,您可以在显示过时的结果列表时添加一个视觉指示。
<div style={{
opacity: query !== deferredQuery ? 0.5 : 1,
}}>
<SearchResults query={deferredQuery} />
</div>
有了这个更改,只要您开始键入,过时的结果列表就会稍微变暗,直到新的结果列表加载完毕。您还可以添加 CSS 过渡来延迟变暗,使其感觉更渐变,如下例所示。
import { Suspense, useState, useDeferredValue } from 'react'; import SearchResults from './SearchResults.js'; export default function App() { const [query, setQuery] = useState(''); const deferredQuery = useDeferredValue(query); const isStale = query !== deferredQuery; return ( <> <label> Search albums: <input value={query} onChange={e => setQuery(e.target.value)} /> </label> <Suspense fallback={<h2>Loading...</h2>}> <div style={{ opacity: isStale ? 0.5 : 1, transition: isStale ? 'opacity 0.2s 0.2s linear' : 'opacity 0s 0s linear' }}> <SearchResults query={deferredQuery} /> </div> </Suspense> </> ); }
延迟 UI 部分的重新渲染
您还可以将 useDeferredValue
用作性能优化。当 UI 的一部分重新渲染速度很慢,没有简单的方法对其进行优化,并且您希望防止它阻塞 UI 的其余部分时,它非常有用。
假设您有一个文本字段和一个组件(例如图表或长列表),它们在每次按键时都会重新渲染。
function App() {
const [text, setText] = useState('');
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
<SlowList text={text} />
</>
);
}
首先,优化 SlowList
以在它的 props 相同时跳过重新渲染。为此,将其包装在 memo
中:
const SlowList = memo(function SlowList({ text }) {
// ...
});
但是,只有当 SlowList
的 props 与上一次渲染时相同时,这才能有所帮助。您现在面临的问题是,当它们不同时,并且当您实际上需要显示不同的视觉输出时,它会很慢。
具体来说,主要性能问题是,每当您在输入框中键入内容时,SlowList
都会接收新的 props,并且重新渲染其整个树会使键入感觉卡顿。在这种情况下,useDeferredValue
允许您优先更新输入(必须快速)而不是更新结果列表(允许较慢)。
function App() {
const [text, setText] = useState('');
const deferredText = useDeferredValue(text);
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
<SlowList text={deferredText} />
</>
);
}
这不会使 SlowList
的重新渲染速度更快。但是,它告诉 React 可以降低列表重新渲染的优先级,这样就不会阻塞按键。列表将“滞后于”输入,然后“追赶”。和以前一样,React 将尽快尝试更新列表,但不会阻止用户键入。
示例 1的 2: 列表的延迟重新渲染
在这个例子中,SlowList
组件中的每个项目都被人为地放慢了速度,以便您可以看到 useDeferredValue
如何让您保持输入的响应速度。在输入框中键入内容,并注意键入感觉很快,而列表则“滞后于”它。
import { useState, useDeferredValue } from 'react'; import SlowList from './SlowList.js'; export default function App() { const [text, setText] = useState(''); const deferredText = useDeferredValue(text); return ( <> <input value={text} onChange={e => setText(e.target.value)} /> <SlowList text={deferredText} /> </> ); }
深入探讨
在此场景中,您可能之前使用过两种常见的优化技术
- 防抖 (Debouncing) 表示您会在用户停止输入(例如,等待一秒钟)之后再更新列表。
- 节流 (Throttling) 表示您会每隔一段时间(例如,最多每秒一次)更新列表。
虽然这些技术在某些情况下很有帮助,但useDeferredValue
更适合优化渲染,因为它与 React 本身深度集成,并能适应用户的设备。
与防抖或节流不同,它不需要选择任何固定的延迟。如果用户的设备很快(例如,功能强大的笔记本电脑),则延迟的重新渲染几乎会立即发生,并且不会被注意到。如果用户的设备很慢,则列表会根据设备的慢速程度成比例地“滞后”于输入。
此外,与防抖或节流不同,由useDeferredValue
执行的延迟重新渲染默认情况下是可中断的。这意味着,如果 React 正在重新渲染一个大型列表,但用户进行了另一个按键操作,React 将放弃该重新渲染,处理按键操作,然后再次在后台开始渲染。相比之下,防抖和节流仍然会产生卡顿的体验,因为它们是阻塞的:它们只是推迟了渲染阻塞按键操作的时刻。
如果您正在优化的工作不是在渲染期间发生的,则防抖和节流仍然有用。例如,它们可以减少您发出的网络请求次数。您还可以将这些技术结合使用。