likes
comments
collection
share

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

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

众所周知(其实很多人没有意识到),React的默认渲染行为是同步的,React总是在我一个操作或者状态更新后再执行下一个操作或状态更新。在大部分场景下,这种即时响应是理想的,因为它确保了UI与内部状态保持紧密同步。但在密集或资源繁重的更新场景下——如加载大量数据时,这种同步行为可能导致整个应用的UI卡顿甚至没有响应。我们把这种UI卡顿现象叫做“阻塞”,当你遇到这种情况时,你的leader往往会让你进行用户体验优化🐶。

React v18开始,官方引入了并发模式(Concurrent Mode)和一些相关的Hooks,其中之一就是useTransition。开发者可以将某些状态更新标记为“可中断”的,从而允许React在必要时打断这些更新,先处理其他更为紧急的任务。我们把这种渲染效果称作“非阻塞UI”。

在这篇文章中,我们就来探讨useTransition的工作原理,如何在实际应用中使用它。

useTransition基础

首先,我们要明白useTransition的设计初衷:在React的并发模式下,允许我们中断或延后某些状态更新,以便于能够在长时间的计算或数据拉取时保持UI的响应性。

用法定义

const [isPending, startTransition] = useTransition()
  • isPending:是一个布尔值,当过渡状态仍在进行中时,其值为true;否则为false
  • startTransition:是一个函数,当你希望启动一个新的过渡状态时调用它。

工作原理

  1. 并发模式下的状态更新分类: 在React的并发模式中,不是所有的状态更新都被视为等同的。React将更新分为不同的优先级类别,其中某些更新(如输入处理)被认为是更加紧急的,而其他更新(如从服务器获取数据)则可以中断或者延后更新。
  2. 使用startTransition进行状态更新: 当你使用startTransition函数进行状态更新时,你实际上告诉 React:这个更新不是非常紧急的,如果有更重要的更新要处理,你可以中断或延后这个次要更新。
  3. isPending的用途: isPending 为我们提供了一个标识,告诉我们是否有一个startTransition正在执行。我们可以根据isPending来设置过渡状态的样式。

使用范围和注意事项

  • 只有当你能访问某个状态的set函数时,你才能将更新包装进useTransition中。

  • 传递给startTransition的函数必须是同步的,而不能是异步的。

    startTransition(async () => {
      await someAsyncFunction();
      // ❌ Setting state *after* startTransition call
      setPage('/about');
    });
    
    await someAsyncFunction();
    startTransition(() => {
      // ✅ Setting state *during* startTransition call
      setPage('/about');
    });
    
  • 如果你想根据某个 prop 或自定义 Hook 的值来启动一个过渡,那么你应该尝试使用useDeferredValue。这是下一篇介绍的hook,此处不展开。

  • 不能用于控制文本输入。因为输入框是需要实时更新的,如果用useTransition降低了渲染优先级,可能造成输入“卡顿”。

  • 不要在startTransition内部使用setTimeout,如果一定要用setTimeout,你可以在startTransition外层使用

    startTransition(() => {
      // ❌ Setting state *after* startTransition call
      setTimeout(() => {
        setPage('/about');
      }, 1000);
    });
    
    setTimeout(() => {
      startTransition(() => {
        // ✅ Setting state *during* startTransition call
        setPage('/about');
      });
    }, 1000);
    
  • 前面说到很多次“中断或延后更新”,那么什么时候中断,什么时候延后更新?最简单的理解:被useTransition包裹的同一个状态多次更新,只会渲染最后一个,前面的都算中断(仅UI层面,如:长列表多次请求);不同组件触发不同状态的更新,被useTransition包裹的状态优先级较低,被中断后会等高优先级的状态更新完成后继续更新(如:复杂图表渲染被中断,会在高优先级状态更新后,继续处理图表的渲染)。

示例

有无useTransition的对比

想象一个场景,页面上有多个tab,其中一个请求耗时较长,我们快速点击tab切换内容,但总是会在请求耗时的tab上卡顿一下,代码如下:

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

const TabContainer = () => {
  const [tab, setTab] = useState('about');

	// 核心:切换选项卡
  function selectTab(nextTab) {
    setTab(nextTab);
  }

  return (
    <div>
      <div>
        <TabButton isActive={tab === 'about'} onClick={() => selectTab('about')}>About</TabButton>
        <TabButton isActive={tab === 'posts'} onClick={() => selectTab('posts')}>Posts (slow)</TabButton>
        <TabButton isActive={tab === 'contact'} onClick={() => selectTab('contact')}>Contact</TabButton>
      </div>
      <hr />
      {tab === 'about' && <AboutTab />}
      {tab === 'posts' && <PostsTab />}
      {tab === 'contact' && <ContactTab />}
    </div>
  );
};

const PostsTab = memo(() => {
  let items = [];
  for (let i = 0; i < 500; i++) {
    items.push(<SlowPost key={i} index={i} />);
  }
  return <ul>{items}</ul>;
});

const SlowPost = ({ index }) => {
  let startTime = performance.now();
  while (performance.now() - startTime < 10) { }
  return <li>Post on weijunext.com #{index + 1}</li>;
};

// 省略非关键代码
// ……

export default TabContainer;

如果我们想用useTransition保持UI的响应,只需要用startTransition包裹切换选项卡的set

const [isPending, startTransition] = React.useTransition();

function selectTab(nextTab) {
  startTransition(() => {
    setTab(nextTab);      
  });
}

这样我们快速切换tab,无论点到哪一个tab都不会卡顿。

useTransitionSuspense实现路由流畅切换

当与路由结合使用时,React.Suspense允许我们延迟渲染新的路由内容,直到所需的数据被完全加载。而useTransition则允许我们可控地开始这种可能导致UI变化的过渡——导航到新页面。

例如这个示例:

import React, { useState } from 'react';
import { BrowserRouter, Switch, Route, Link } from 'react-router-dom';

const [location, setLocation] = useState(window.location);
const [isPending, startTransition] = React.unstable_useTransition();

// 使用 startTransition 来更新 location 状态,能够延迟显示新页面的内容,直到数据加载完毕
function handleNavigation(newLocation) {
  startTransition(() => {
    setLocation(newLocation);
  });
}

// 主应用组件
function App() {
  return (
    <div>
      <BrowserRouter>
        {/* 导航 */}
        <nav>
          <CustomLink to="/about">About</CustomLink>
          <CustomLink to="/contact">Contact</CustomLink>
        </nav>

        {/* 使用 React.Suspense 来处理组件的懒加载 */}
        <React.Suspense fallback={<LoadingIndicator />}>
          <Switch location={location}>
            <Route path="/about" component={AboutPage} />
            <Route path="/contact" component={ContactPage} />
            {/* ...其他路由... */}
          </Switch>
        </React.Suspense>

        {/* 使用 isPending 显示或隐藏全局加载指示器 */}
        {isPending && <LoadingIndicator />}
      </BrowserRouter>
    </div>
  );
}

export default App;

通过这种方式,我们可以优雅地处理路由切换,并确保在数据加载时为用户提供一个流畅的体验。

结语

useTransition带来的是“可中断”的异步UI渲染,当实际工作中遇到卡顿的现象,不妨想想是否能用useTransition解决。

系列文章列表

未完待续……