useDeferredValue

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 将首先更新延迟值就重新渲染,然后尝试在后台使用新接收的值重新渲染。

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

注意

此示例假设您使用的是启用 Suspense 的数据源

  • 使用启用 Suspense 的框架(如RelayNext.js)进行数据获取
  • 使用lazy延迟加载组件代码
  • 使用use读取 Promise 的值

了解有关 Suspense 及其限制的更多信息。

在此示例中,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>
    </>
  );
}

深入探讨

延迟值在底层是如何工作的?

您可以将其视为分两步完成

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

  2. 在后台,React 尝试使用更新后的 querydeferredQuery 值 ("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 将尽快尝试更新列表,但不会阻止用户键入。

useDeferredValue 和未优化的重新渲染之间的区别

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

陷阱

此优化需要将 SlowList 包装在 memo 中。 这是因为每当 text 发生更改时,React 都需要能够快速重新渲染父组件。在重新渲染期间,deferredText 仍然具有其先前的值,因此 SlowList 能够跳过重新渲染(其 props 没有更改)。如果没有 memo 它无论如何都必须重新渲染,这会使优化失去意义。

深入探讨

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

在此场景中,您可能之前使用过两种常见的优化技术

  • 防抖 (Debouncing) 表示您会在用户停止输入(例如,等待一秒钟)之后再更新列表。
  • 节流 (Throttling) 表示您会每隔一段时间(例如,最多每秒一次)更新列表。

虽然这些技术在某些情况下很有帮助,但useDeferredValue更适合优化渲染,因为它与 React 本身深度集成,并能适应用户的设备。

与防抖或节流不同,它不需要选择任何固定的延迟。如果用户的设备很快(例如,功能强大的笔记本电脑),则延迟的重新渲染几乎会立即发生,并且不会被注意到。如果用户的设备很慢,则列表会根据设备的慢速程度成比例地“滞后”于输入。

此外,与防抖或节流不同,由useDeferredValue执行的延迟重新渲染默认情况下是可中断的。这意味着,如果 React 正在重新渲染一个大型列表,但用户进行了另一个按键操作,React 将放弃该重新渲染,处理按键操作,然后再次在后台开始渲染。相比之下,防抖和节流仍然会产生卡顿的体验,因为它们是阻塞的:它们只是推迟了渲染阻塞按键操作的时刻。

如果您正在优化的工作不是在渲染期间发生的,则防抖和节流仍然有用。例如,它们可以减少您发出的网络请求次数。您还可以将这些技术结合使用。