useDeferredValue 是一个 React Hook,可以让你延迟更新 UI 的某个部分。
const deferredValue = useDeferredValue(value)参考
🌐 Reference
useDeferredValue(value, initialValue?)
在组件的顶层调用 useDeferredValue 以获取该值的延迟版本。
🌐 Call useDeferredValue at the top level of your component to get a deferred version of that value.
import { useState, useDeferredValue } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
// ...
}参数
🌐 Parameters
value:你想要延迟的值。它可以是任何类型。- 可选
initialValue:在组件的初始渲染期间使用的值。如果省略此选项,useDeferredValue在初始渲染期间不会延迟,因为没有可以替代渲染的value的之前版本。
返回
🌐 Returns
currentValue:在初次渲染期间,返回的延迟值将是initialValue,或者与你提供的值相同。在更新期间,React 会首先尝试使用旧值重新渲染(因此它将返回旧值),然后在后台尝试使用新值再次重新渲染(因此它将返回更新后的值)。
注意事项
🌐 Caveats
- 当更新位于过渡中时,
useDeferredValue总是返回新的value,并且不会生成延迟渲染,因为更新已经被延迟。 - 你传给
useDeferredValue的值应该是原始值(比如字符串和数字)或者是在渲染之外创建的对象。如果你在渲染过程中创建一个新对象并立即传给useDeferredValue,每次渲染时它都会不同,从而导致不必要的后台重新渲染。 - 当
useDeferredValue接收到与Object.is不同的值时,除了当前渲染(此时仍使用先前的值)之外,它还会在后台安排一次使用新值的重新渲染。后台重新渲染是可中断的:如果value又有了新的更新,React 将从头重新启动后台重新渲染。例如,如果用户在输入框中输入的速度比图表接收其延迟值并重新渲染的速度快,图表将仅在用户停止输入后才重新渲染。 useDeferredValue已与<Suspense>集成。如果新值引起的后台更新导致 UI 暂停,用户将看不到回退值。他们将看到旧的延迟值,直到数据加载完成。useDeferredValue本身不能阻止额外的网络请求。useDeferredValue本身不会导致固定延迟。一旦 React 完成原始的重新渲染,React 将立即开始使用新的延迟值进行后台重新渲染。由事件(如输入)引起的任何更新将中断后台重新渲染,并优先处理这些更新。useDeferredValue引起的后台重新渲染在提交到屏幕之前不会触发 Effects。如果后台重新渲染被挂起,其 Effects 会在数据加载和 UI 更新之后运行。
用法
🌐 Usage
在加载新内容时显示旧内容
🌐 Showing stale content while fresh content is loading
在组件的顶层调用 useDeferredValue 以延迟更新 UI 的某些部分。
🌐 Call useDeferredValue at the top level of your component to defer updating some part of your UI.
import { useState, useDeferredValue } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
// ...
}在初始渲染期间, 延迟值 将与你提供的 值 相同。
在更新期间, 延迟值 会“落后于”最新的 值。特别是,React 会首先在不更新延迟值的情况下重新渲染,然后尝试在后台使用新接收到的值重新渲染。
让我们通过一个例子来看看这什么时候有用。
在此示例中,SearchResults 组件在获取搜索结果时会suspend。尝试输入 "a",等待结果,然后再将其编辑为 "ab"。"a" 的结果会被加载回退所替代。
🌐 In this example, the SearchResults component suspends while fetching the search results. Try typing "a", waiting for the results, and then editing it to "ab". The results for "a" get replaced by the loading fallback.
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> </> ); }
一个常见的替代用户界面模式是延迟更新结果列表,并在新结果准备好之前继续显示之前的结果。调用 useDeferredValue 将查询的延迟版本传递下去:
🌐 A common alternative UI pattern is to defer updating the list of results and to keep showing the previous results until the new results are ready. Call useDeferredValue to pass a deferred version of the query down:
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 会显示一段时间的陈旧结果。
🌐 The query will update immediately, so the input will display the new value. However, the deferredQuery will keep its previous value until the data has loaded, so SearchResults will show the stale results for a bit.
在下面的示例中输入 "a",等待结果加载,然后将输入编辑为 "ab"。注意,现在你会看到旧的结果列表,而不是 Suspense 回退内容,直到新结果加载完成:
🌐 Enter "a" in the example below, wait for the results to load, and then edit the input to "ab". Notice how instead of the Suspense fallback, you now see the stale result list until the new results have loaded:
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> </> ); }
深入研究
🌐 How does deferring a value work under the hood?
你可以认为它分两步发生:
🌐 You can think of it as happening in two steps:
- 首先,React 使用新的
query("ab")重新渲染,但使用旧的deferredQuery(仍然是"a")。 你传递给结果列表的deferredQuery值是延迟的:它“落后于”query值。 - 在后台,React 会尝试在
query和deferredQuery都更新为"ab"的情况下重新渲染。 如果这次重新渲染完成,React 会在屏幕上显示它。但是,如果渲染被挂起("ab"的结果尚未加载),React 会放弃这次渲染尝试,并在数据加载完成后再次尝试重新渲染。用户将持续看到过时的延迟值,直到数据准备就绪。
延迟的“背景”渲染是可中断的。例如,如果你再次在输入框中输入,React 将放弃当前渲染并使用新的值重新开始。React 将始终使用最新提供的值。
🌐 The deferred “background” rendering is interruptible. For example, if you type into the input again, React will abandon it and restart with the new value. React will always use the latest provided value.
请注意,每次按键仍然会发出网络请求。这里被延迟的是显示结果(直到它们准备好),而不是网络请求本身。即使用户继续输入,每次按键的响应都会被缓存,因此按下退格键是即时的,不会再次获取。
🌐 Note that there is still a network request per each keystroke. What’s being deferred here is displaying results (until they’re ready), not the network requests themselves. Even if the user continues typing, responses for each keystroke get cached, so pressing Backspace is instant and doesn’t fetch again.
表明内容是旧的
🌐 Indicating that the content is stale
在上面的示例中,没有任何迹象表明最新查询的结果列表仍在加载。如果新结果加载需要一些时间,这可能会让用户感到困惑。为了更清楚地向用户显示结果列表与最新查询不匹配,可以在显示过期结果列表时添加视觉提示:
🌐 In the example above, there is no indication that the result list for the latest query is still loading. This can be confusing to the user if the new results take a while to load. To make it more obvious to the user that the result list does not match the latest query, you can add a visual indication when the stale result list is displayed:
<div style={{
opacity: query !== deferredQuery ? 0.5 : 1,
}}>
<SearchResults query={deferredQuery} />
</div>有了这个更改,一旦你开始输入,旧的结果列表会稍微变暗,直到新的结果列表加载完成。你也可以添加一个 CSS 过渡来延迟变暗,这样感觉会更渐进,就像下面的示例一样:
🌐 With this change, as soon as you start typing, the stale result list gets slightly dimmed until the new result list loads. You can also add a CSS transition to delay dimming so that it feels gradual, like in the example below:
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 的重新渲染
🌐 Deferring re-rendering for a part of the UI
你也可以将 useDeferredValue 用作性能优化。当你的部分 UI 重新渲染很慢,没有简单的方法可以优化它,并且你希望防止它阻塞其余的 UI 时,它非常有用。
🌐 You can also apply useDeferredValue as a performance optimization. It is useful when a part of your UI is slow to re-render, there’s no easy way to optimize it, and you want to prevent it from blocking the rest of the UI.
假设你有一个文本字段和一个组件(如图表或长列表),它们在每次击键时重新渲染:
🌐 Imagine you have a text field and a component (like a chart or a long list) that re-renders on every keystroke:
function App() {
const [text, setText] = useState('');
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
<SlowList text={text} />
</>
);
}首先,优化 SlowList,以便当其 props 相同时跳过重新渲染。要做到这一点,将其封装在 memo 中:
🌐 First, optimize SlowList to skip re-rendering when its props are the same. To do this, wrap it in memo:
const SlowList = memo(function SlowList({ text }) {
// ...
});然而,这只有在 SlowList 属性与上一次渲染时相同时才有帮助。你现在面临的问题是,当它们不同时,并且你实际上需要显示不同的视觉输出时,它会变慢。
🌐 However, this only helps if the SlowList props are the same as during the previous render. The problem you’re facing now is that it’s slow when they’re different, and when you actually need to show different visual output.
具体来说,主要的性能问题是,每当你在输入框中输入内容时,SlowList 会接收到新的 props,并重新渲染其整个树,这会导致输入感觉卡顿。在这种情况下,useDeferredValue 允许你优先更新输入(必须快速)而不是更新结果列表(允许更慢):
🌐 Concretely, the main performance problem is that whenever you type into the input, the SlowList receives new props, and re-rendering its entire tree makes the typing feel janky. In this case, useDeferredValue lets you prioritize updating the input (which must be fast) over updating the result list (which is allowed to be slower):
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 会尽快尝试更新列表,但不会阻碍用户输入。
🌐 This does not make re-rendering of the SlowList faster. However, it tells React that re-rendering the list can be deprioritized so that it doesn’t block the keystrokes. The list will “lag behind” the input and then “catch up”. Like before, React will attempt to update the list as soon as possible, but will not block the user from typing.
例子 1 of 2: 延迟重新渲染列表
🌐 Deferred re-rendering of the list
在这个示例中,SlowList 组件中的每个项目都被人为地减慢,以便你可以看到 useDeferredValue 如何让输入保持响应。输入内容时,请注意输入感觉很灵敏,而列表“落后”于输入。
🌐 In this example, each item in the SlowList component is artificially slowed down so that you can see how useDeferredValue lets you keep the input responsive. Type into the input and notice that typing feels snappy while the list “lags behind” it.
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} /> </> ); }
深入研究
🌐 How is deferring a value different from debouncing and throttling?
在这种情况下,你之前可能使用过两种常见的优化技术:
🌐 There are two common optimization techniques you might have used before in this scenario:
- 防抖 意味着你会等待用户停止输入(例如一秒钟)后再更新列表。
- 节流 意味着你会不时更新列表(例如,最多每秒一次)。
虽然这些技术在某些情况下很有用,但 useDeferredValue 更适合优化渲染,因为它与 React 本身紧密集成,并能够适应用户的设备。
🌐 While these techniques are helpful in some cases, useDeferredValue is better suited to optimizing rendering because it is deeply integrated with React itself and adapts to the user’s device.
与防抖或节流不同,它不需要选择任何固定延迟。如果用户的设备很快(例如高性能注意本),延迟的重新渲染几乎会立即发生,且不会被察觉。如果用户的设备很慢,列表会根据设备的慢速程度相应地“滞后”于输入。
🌐 Unlike debouncing or throttling, it doesn’t require choosing any fixed delay. If the user’s device is fast (e.g. powerful laptop), the deferred re-render would happen almost immediately and wouldn’t be noticeable. If the user’s device is slow, the list would “lag behind” the input proportionally to how slow the device is.
另外,与防抖或节流不同,useDeferredValue 执行的延迟重新渲染默认是可中断的。这意味着,如果 React 正在重新渲染一个大型列表,而用户又进行了另一次按键操作,React 会放弃当前的重新渲染,先处理按键操作,然后再在后台重新开始渲染。相比之下,防抖和节流仍然会产生卡顿的体验,因为它们是阻塞的:它们只是推迟了渲染阻塞按键操作的时刻。
🌐 Also, unlike with debouncing or throttling, deferred re-renders done by useDeferredValue are interruptible by default. This means that if React is in the middle of re-rendering a large list, but the user makes another keystroke, React will abandon that re-render, handle the keystroke, and then start rendering in the background again. By contrast, debouncing and throttling still produce a janky experience because they’re blocking: they merely postpone the moment when rendering blocks the keystroke.
如果你优化的工作不是在渲染过程中发生的,防抖和节流仍然有用。例如,它们可以让你发送更少的网络请求。你也可以将这些技术一起使用。
🌐 If the work you’re optimizing doesn’t happen during rendering, debouncing and throttling are still useful. For example, they can let you fire fewer network requests. You can also use these techniques together.