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

🧑‍💻
推荐全栈学习资源:
  • Next.js 中文文档:样式和官网一样的中文文档,创造沉浸式Next.js中文学习体验。
  • 《Chrome插件全栈开发》:真实出海项目的实战教学课,讲解Chrome插件和Next.js端的全栈开发,帮助你半个月内成为全栈出海工程师。
  • 上一篇文章(《精读React hook(九):使用useTransition进行非阻塞渲染》)我们学习了,在React v18的并发模式下,使用useTransition可以在密集计算的场景下让UI无阻塞。React v18还引入了一个和useTransition相似的hook,就是useDeferredValue。为什么说相似呢,因为useDeferredValue的作用是:延迟某个值的更新,使高优先级的任务可以先行完成。

    开始学习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

    专栏资源

    专栏博客地址:精读React Hooks

    专栏演示站:React Hooks Demos

    专栏源码仓库:👉Github - Source Code

    交个朋友:👉加入「独立全栈交流群」

    专栏文章列表:

    精读React hooks(一):useState 的几个基础用法和进阶技巧

    精读React hooks(二):React状态管理的强大工具——useReducer

    精读React hooks(三):useContext从基础应用到性能优化

    精读React hooks(四):useRef的多维用途

    精读React hooks(五):useEffect使用细节知多少?

    精读React hooks(六):useLayoutEffect解决了什么问题?

    精读React hooks(七):用useMemo来减少性能开销

    精读React hooks(八):我们为什么需要useCallback

    精读React hooks(九):使用useTransition进行非阻塞渲染

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

    精读React hooks(十一):useInsertionEffect——CSS-in-JS样式注入新方式

    精读React hooks(十二):使用useImperativeHandle能获得什么能力

    精读React hooks(十三):使用useSyncExternalStore获取实时数据

    精读React hooks(十四):总有一天你会需要useId为你生成唯一id

    精读React hooks(十五):把useDebugValue加入你的React调试工具库

    精读React hooks(十六):一个为代码优雅而生的hook——use