useDeferredValue

useDeferredValue 是一个 React 钩子,可让你推迟更新 UI 的一部分。

¥useDeferredValue is a React Hook that lets you defer updating a part of the 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);
// ...
}

请参阅下面的更多示例。

¥See more examples below.

参数

¥Parameters

  • value:你要延迟的值。它可以有任何类型。

    ¥value: The value you want to defer. It can have any type.

Canary only

可选 initialValue:在组件初始渲染期间使用的值。如果省略此选项,useDeferredValue 将不会在初始渲染期间推迟,因为没有可以替代渲染的 value 的先前版本。

¥optional initialValue: A value to use during the initial render of a component. If this option is omitted, useDeferredValue will not defer during the initial render, because there’s no previous version of value that it can render instead.

返回

¥Returns

  • currentValue:在初始渲染期间,返回的延迟值将与你提供的值相同。在更新期间,React 将首先尝试使用旧值重新渲染(因此它将返回旧值),然后在后台尝试使用新值进行另一次重新渲染(因此它将返回更新后的值)。

    ¥currentValue: During the initial render, the returned deferred value will be the same as the value you provided. During updates, React will first attempt a re-render with the old value (so it will return the old value), and then try another re-render in the background with the new value (so it will return the updated value).

Canary

在最新的 React Canary 版本中,useDeferredValue 在初始渲染时返回 initialValue,并在后台安排重新渲染并返回 value

¥In the latest React Canary versions, useDeferredValue returns the initialValue on initial render, and schedules a re-render in the background with the value returned.

注意事项

¥Caveats

  • 当更新在 Transition 中时,useDeferredValue 始终返回新的 value 并且不会产生延迟渲染,因为更新已被延迟。

    ¥When an update is inside a Transition, useDeferredValue always returns the new value and does not spawn a deferred render, since the update is already deferred.

  • 你传递给 useDeferredValue 的值应该是原始值(如字符串和数字)或在渲染之外创建的对象。如果你在渲染期间创建一个新对象并立即将其传递给 useDeferredValue,则每次渲染时都会有所不同,从而导致不必要的背景重新渲染。

    ¥The values you pass to useDeferredValue should either be primitive values (like strings and numbers) or objects created outside of rendering. If you create a new object during rendering and immediately pass it to useDeferredValue, it will be different on every render, causing unnecessary background re-renders.

  • useDeferredValue 收到不同的值(与 Object.is 相比)时,除了当前渲染(当它仍然使用以前的值时)之外,它还会安排在后台使用新值重新渲染。后台重新渲染是可中断的:如果 value 有另一个更新,React 将从头开始重新启动后台重新渲染。例如,如果用户键入输入的速度快于接收延迟值的图表可以重新渲染的速度,则图表只会在用户停止输入后重新渲染。

    ¥When useDeferredValue receives a different value (compared with Object.is), in addition to the current render (when it still uses the previous value), it schedules a re-render in the background with the new value. The background re-render is interruptible: if there’s another update to the value, React will restart the background re-render from scratch. For example, if the user is typing into an input faster than a chart receiving its deferred value can re-render, the chart will only re-render after the user stops typing.

  • useDeferredValue<Suspense> 集成 如果新值引起的后台更新暂停了 UI,用户将看不到回退。在数据加载之前,它们将看到旧的延迟值。

    ¥useDeferredValue is integrated with <Suspense>. If the background update caused by a new value suspends the UI, the user will not see the fallback. They will see the old deferred value until the data loads.

  • useDeferredValue 本身并不能阻止额外的网络请求。

    ¥useDeferredValue does not by itself prevent extra network requests.

  • useDeferredValue 本身没有固定延迟。一旦 React 完成原始重新渲染,React 将立即开始使用新的延迟值进行后台重新渲染。由事件(如类型)引起的任何更新都会中断后台重新渲染并优先于它。

    ¥There is no fixed delay caused by useDeferredValue itself. As soon as React finishes the original re-render, React will immediately start working on the background re-render with the new deferred value. Any updates caused by events (like typing) will interrupt the background re-render and get prioritized over it.

  • useDeferredValue 引起的背景重新渲染在提交到屏幕之前不会触发副作用。如果后台重新渲染挂起,其副作用将在数据加载和 UI 更新后运行。

    ¥The background re-render caused by useDeferredValue does not fire Effects until it’s committed to the screen. If the background re-render suspends, its Effects will run after the data loads and the UI updates.


用法

¥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);
// ...
}

在初始渲染期间,延迟值 将与你提供的 相同。

¥During the initial render, the deferred value will be the same as the value you provided.

在更新期间,延迟值 将 “滞后” 最新的 。特别是,React 将首先重新渲染而不更新延迟值,然后尝试在后台使用新接收到的值重新渲染。

¥During updates, the deferred value will “lag behind” the latest value. In particular, React will first re-render without updating the deferred value, and then try to re-render with the newly received value in the background.

让我们通过一个例子来看看这在什么时候有用。

¥Let’s walk through an example to see when this is useful.

注意

此示例假设你使用启用 Suspense 的数据源:

¥This example assumes you use a Suspense-enabled data source:

  • 使用支持 Suspense 的框架(如 RelayNext.js)获取数据

    ¥Data fetching with Suspense-enabled frameworks like Relay and Next.js

  • 使用 lazy 延迟加载组件代码

    ¥Lazy-loading component code with lazy

  • 使用 use 读取 Promise 的值

    ¥Reading the value of a Promise with use

详细了解 Suspense 及其局限性。

¥Learn more about Suspense and its limitations.

在此示例中,SearchResults 组件 suspends 同时获取搜索结果。尝试键入 "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>
    </>
  );
}

一种常见的替代 UI 模式是推迟更新结果列表并继续显示以前的结果,直到新结果准备就绪。调用 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:

  1. 首先,React 使用新的 query ("ab") 重新渲染,但使用旧的 deferredQuery(仍然是 "a")。你传递给结果列表的 deferredQuery 值被延迟:它 “滞后” query 值。

    ¥First, React re-renders with the new query ("ab") but with the old deferredQuery (still "a"). The deferredQuery value, which you pass to the result list, is deferred: it “lags behind” the query value.

  2. 在后台,React 尝试重新渲染,并将 querydeferredQuery 更新为 "ab"。如果重新渲染完成,React 会将其显示在屏幕上。但是,如果它挂起("ab" 的结果尚未加载),React 将放弃此渲染尝试,并在数据加载后再次尝试重新渲染。在数据准备好之前,用户将一直看到旧的延迟值。

    ¥In the background, React tries to re-render with both query and deferredQuery updated to "ab". If this re-render completes, React will show it on the screen. However, if it suspends (the results for "ab" have not loaded yet), React will abandon this rendering attempt, and retry this re-render again after the data has loaded. The user will keep seeing the stale deferred value until the data is ready.

延迟的 “background” 渲染是可中断的。例如,如果你再次键入输入,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 以在其属性相同时跳过重新渲染。为此,将其封装在 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 都会收到新的属性,重新渲染它的整个树会让输入感觉很卡顿。在这种情况下,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.

The difference between useDeferredValue and unoptimized re-rendering

例子 1 / 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} />
    </>
  );
}

易犯错误

这种优化要求 SlowList 被封装在 memo 中,这是因为每当 text 发生变化时,React 需要能够快速地重新渲染父组件。在重新渲染期间,deferredText 仍然具有其先前的值,因此 SlowList 能够跳过重新渲染(它的属性没有改变)。如果没有 memo,,它无论如何都必须重新渲染,从而破坏了优化点。

¥This optimization requires SlowList to be wrapped in memo. This is because whenever the text changes, React needs to be able to re-render the parent component quickly. During that re-render, deferredText still has its previous value, so SlowList is able to skip re-rendering (its props have not changed). Without memo, it would have to re-render anyway, defeating the point of the optimization.

深入研究

延迟值与去抖动和节流有何不同?

¥How is deferring a value different from debouncing and throttling?

在这种情况下,你之前可能使用过两种常见的优化技术:

¥There are two common optimization techniques you might have used before in this scenario:

  • 去抖动意味着你会在更新列表之前等待用户停止输入(例如一秒钟)。

    ¥Debouncing means you’d wait for the user to stop typing (e.g. for a second) before updating the list.

  • 节流意味着你会每隔一段时间更新一次列表(例如最多每秒一次)。

    ¥Throttling means you’d update the list every once in a while (e.g. at most once a second).

虽然这些技术在某些情况下很有用,但 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.


React 中文网 - 粤ICP备13048890号