React18新hook:useDeferredValue

799 阅读3分钟

useDeferredValue 的基础用法

useDeferredValue 可以让你延迟更新 UI 的某些部分

用法如下:

const deferredValue = useDeferredValue(someValue);

其中someValue是你想要延迟的值,它可以是任何类型。

deferredValue的渲染有两种情况:

  1. 在初始渲染时deferredValue的值将与someValue的值相同。
  2. 在UI更新期间,因为deferredValue的优先级较低,即使并发模式下deferredValue已在后台更新,React也会先使用旧值渲染,当其它高优先级的状态更新完成,才会把deferredValue新值渲染出来。

useDeferredValue 应用示例

在新内容加载期间显示旧内容

比如,在搜索时,我们可能会配合 Suspense 组件,当发起搜索请求时,展示 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>
    </>
  );
}

像这样,有一个 loading 效果:

image.png

然而,我们可以通过 useDeferredValue,在搜索时,延迟更新的结果也就是在发送搜索请求时,不展示 loading,旧数据依然展示(下面的例子我们给旧数据置灰),当请求完毕时,再去渲染最新的数据

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>
      <div style={{
          opacity: isStale ? 0.5 : 1,
          transition: isStale ? 'opacity 0.2s 0.2s linear' : 'opacity 0s 0s linear'
        }}>
          <SearchResults query={deferredQuery} />
      </div>
    </>
  );
}
  • 当搜索 a 时:

image.png

  • 然后再搜索 ab 时,旧数据置灰,当请求成功时,再渲染最新的数据

image.png

image.png

延迟渲染 UI 的某些部分

你还可以将 useDeferredValue 作为性能优化的手段。当你的 UI 某个部分重新渲染很慢、没有简单的优化方法,同时你又希望避免它阻塞其他 UI 的渲染时,使用 useDeferredValue 很有帮助。

想象一下,你有一个文本框和一个组件(例如图表或长列表),在每次按键时都会重新渲染:

// app.jsx
import { useState, useDeferredValue } from 'react';
import SlowList from './SlowList.js';

export default function App() {
  const [text, setText] = useState('');
  return (
    <>
      <input value={text} onChange={e => setText(e.target.value)} />
      <SlowList text={text} />
    </>
  );
}

每次你输入的时候,SlowList 都会接受到新的 props,然后重新渲染,就会导致卡顿,然而,我们可以通过 useDeferredValue 去做延迟渲染

// app.jsx
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.jsx
import { memo } from 'react';

const SlowList = memo(function SlowList({ text }) {
  // 仅打印一次。实际的减速是在 SlowItem 组件内部。
  console.log('[ARTIFICIALLY SLOW] Rendering 250 <SlowItem />');

  let items = [];
  for (let i = 0; i < 250; i++) {
    items.push(<SlowItem key={i} text={text} />);
  }
  return (
    <ul className="items">
      {items}
    </ul>
  );
});

function SlowItem({ text }) {
  let startTime = performance.now();
  while (performance.now() - startTime < 1) {
    // 每个 item 暂停 1ms,模拟极其缓慢的代码
  }

  return (
    <li className="item">
      Text: {text}
    </li>
  )
}

export default SlowList;

效果如下:

  • 在输入停止前,不会 rerender image.png

  • 输入停止后,再渲染 image.png

useDeferredValue 和 节流、防抖的区别

从上面的学习可以看出来,useDeferredValue在某些场景下是可以替换掉节流防抖

防抖与节流的局限性

  • 这两种方法都是为了控制函数的执行频率,但它们是阻塞的,可能会导致不流畅的用户体验。

useDeferredValue的优势

  • 它与React深度集成,可以适应用户的设备。如果设备性能好,延迟的重新渲染会很快完成;如果设备性能差,重新渲染会相应地延迟。
  • 它不需要选择固定的延迟,与防抖和节流不同。
  • useDeferredValue执行的重新渲染是可中断的。这意味着在React重新渲染期间,如果发生了其他更新,React会中断当前的渲染并处理新的更新。

适用场景

  • 如果要优化的工作不是在渲染期间进行的,例如减少网络请求,那么防抖和节流仍然是有用的。
  • 如果优化的目标是和渲染有关的,建议使用useDeferredValue