likes
comments
collection
share

精读React hook(十):使用useDeferredValue来做状态延迟更新

作者站长头像
站长
· 阅读数 12

开始学习useDeferredValue之前,我们先明确useTransitionuseDeferredValue的差异:

  • useTransition主要关注点是状态的过渡。它允许开发者控制某个更新的延迟更新,还提供了过渡标识,让开发者能够添加过渡反馈。
  • useDeferredValue主要关注点是单个值的延迟更新。它允许你把特定状态的更新标记为低优先级。

简而言之,如果你想提供过渡反馈,就用useTransition,如果不需要提供过渡反馈,用useDeferredValue就可以。

useDeferredValue基础使用

const deferredValue = useDeferredValue(someValue);

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

deferredValue的渲染有两种情况:

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

示例分析

想象一个场景,你在大数据量场景下做查询,前端渲染就需要延迟更新列表,还希望只有最后一次查询的数据被保留,这时候useDeferredValue就派上用场了,例如:

import { useState, useDeferredValue, memo } from 'react';

export default function App() {
  const [text, setText] = useState('');
  const deferredText = useDeferredValue(text);
  return (
    <>
      {/* 输入框的值直接与 text 绑定,所以输入框会实时显示用户的输入 */}
      <input value={text} onChange={e => setText(e.target.value)} />
      {/* SlowList 组件接受 deferredText 作为属性,渲染优先级会被降低 */}
      {/* 在 UI 真正更新前,如果 deferredText 被更新多次,也只会保留最后一次的结果 */}
      <SlowList text={deferredText} />
    </>
  );
}

const SlowList = memo(function SlowList({ text }) {
  const arr = [];
  for (let i = 0; i < 200; i++) {
    let startTime = performance.now();
    while (performance.now() - startTime < 1) {}
    if (String(i).includes(text)) {
      arr.push(<li key={i}>{i}</li>);
    }
  }
  return (
    <ul className="items">
      {arr}
    </ul>
  );
});

如果你想对照不用useDeferredValue的效果,只要把<SlowList>修改掉就可以:

<SlowList text={text} />

线上示例可以来<👉我的演示站>体验。

使用useDeferredValue注意事项

  • useDeferredValue仅在开启React并发模式的时候才有效

    // React v18以前
    ReactDOM.render(<app />, rootNode) // ❌ 无法开启useTransition
    
    // React v18
    ReactDOM.createRoot(rootNode).render(<app />) // ✅ 开启useTransition
    
  • 传递给useDeferredValue的值应该是原始值(如字符串和数字)或在渲染外部创建的对象

    为什么对象需要创建在外部创建?这其实和JavaScript的机制有关,即使每次创建相同的对象,JavaScript依然会每次生成新的引用。这意味着每次你在组件的渲染函数中创建一个新的对象并将其传递给useDeferredValue,你都是在传递一个新的、与上一次渲染不同的引用。

    function MyComponent() {
      const obj = { name: 'John' }; // 每次 MyComponent 重新渲染时,都会创建一个新的 obj 对象,它们的引用是不同的
    
    	// useDeferredValue 通过 Object.is 来检查值是否有变化,引用不同的对象会被当作不同的值
      const deferredValue = useDeferredValue(obj);
    
      // ...其他代码...
    }
    
  • 当同一个useDeferredValue在渲染前接收到多次不同的值时,只有最后一个会被渲染。想象一下,前端在做大数据量查询的时候,我们当然希望只有最后一次查询的数据成功渲染。

  • useTransition一样,useDeferredValue只会中断或延迟UI的渲染,不会阻止网络请求

  • useDeferredValue<Suspense>结合使用,在useDeferredValue更新时,<Suspense>的fallback是不会出现的,页面上是继续显示useDeferredValue的旧值。这一点和useTransition不一样。

和节流、防抖的关系

从上面的学习可以看出来,useDeferredValue在某些场景下是可以替换掉节流和防抖的,我们先来分析它们各自的特性,然后看看它们分别适用什么场景?

防抖与节流的局限性

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

useDeferredValue的优势

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

适用场景

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

结语

useDeferredValue带来的是可延迟的状态更新,当实际工作中遇到大数据量渲染的场景,不妨想想是否引入useDeferredValue

系列文章列表

未完待续……