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: 您要延迟的值。它可以是任何类型。
  • Canary only 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.

返回

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

金丝雀

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

注意事项

  • 当更新位于过渡中时,useDeferredValue 始终返回新 value 并且不会产生延迟渲染,因为更新已经延迟。

  • 传递给 useDeferredValue 的值应为基本值(如字符串和数字)或在渲染之外创建的对象。如果在渲染期间创建新对象并立即将其传递给 useDeferredValue,则每次渲染时它都会不同,从而导致不必要的后台重新渲染。

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

  • useDeferredValue 已与 <Suspense>. 集成。如果由新值导致的后台更新暂停 UI,用户将看不到回退。他们将看到旧的延迟值,直到数据加载。

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

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

  • useDeferredValue 导致的后台重新渲染不会触发 Effect,直到它提交到屏幕。如果后台重新渲染暂停,其 Effect 将在数据加载和 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 尝试使用同时更新为 "ab"querydeferredQuery 重新渲染。如果此重新渲染完成,React 将在屏幕上显示它。但是,如果它挂起("ab" 的结果尚未加载),React 将放弃此渲染尝试,并在数据加载后再次重试此重新渲染。在数据准备就绪之前,用户将继续看到陈旧的延迟值。

延迟的“后台”渲染是可以中断的。例如,如果你再次输入内容,React 将放弃它并使用新值重新开始。React 将始终使用提供的最新值。

请注意,每次击键仍然有一个网络请求。此处延迟的是显示结果(直到它们准备就绪),而不是网络请求本身。即使用户继续输入,每个击键的响应也会被缓存,因此按 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 以在道具相同的情况下跳过重新渲染。为此,将其包装在 memo 中:

const SlowList = memo(function SlowList({ text }) {
// ...
});

但是,这只有在 SlowList 道具与前一次渲染期间相同时才有用。你现在面临的问题是,当它们不同时,以及当你实际上需要显示不同的视觉输出时,它很慢。

具体来说,主要的性能问题是每当你输入输入时,SlowList 都会收到新的道具,并重新渲染其整个树,这会让输入感觉不流畅。在这种情况下,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 能够跳过重新渲染(其属性未更改)。如果没有 memo,它无论如何都必须重新渲染,从而违背了优化的目的。

深入了解

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

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

  • 防抖意味着您将等待用户停止键入(例如一秒钟),然后再更新列表。
  • 节流意味着您将时不时更新列表(例如,最多一秒一次)。

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

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

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

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