useTransition

useTransition 是一个帮助你在不阻塞 UI 的情况下更新状态的 React Hook。

const [isPending, startTransition] = useTransition()

参考

useTransition()

在组件顶层调用 useTransition,将某些状态更新标记为 transition。

import { useTransition } from 'react';

function TabContainer() {
const [isPending, startTransition] = useTransition();
// ……
}

参见下方更多示例

参数

useTransition 不需要任何参数。

返回值

useTransition 返回一个由两个元素组成的数组:

  1. isPending,告诉你是否存在待处理的 transition。
  2. startTransition 函数,你可以使用此方法将状态更新标记为 transition。

startTransition 函数

useTransition 返回的 startTransition 函数允许你将状态更新标记为 transition。

function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('about');

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

参数

  • 作用域(scope):一个通过调用一个或多个 set 函数 更新状态的函数。React 会立即不带参数地调用此函数,并将在 scope 调用期间将所有同步安排的状态更新标记为 transition。它们将是非阻塞的,并且 不会显示不想要的加载指示器

返回值

startTransition 不返回任何值。

注意

  • useTransition 是一个 Hook,因此只能在组件或自定义 Hook 内部调用。如果需要在其他地方启动 transition(例如从数据库),请调用独立的 startTransition 函数。

  • 只有在可以访问该状态的 set 函数时,才能将其对应的状态更新包装为 transition。如果你想启用 transition 以响应某个 prop 或自定义 Hook 值,请尝试使用 useDeferredValue

  • 传递给 startTransition 的函数必须是同步的。React 会立即执行此函数,并将在其执行期间发生的所有状态更新标记为 transition。如果在其执行期间,尝试稍后执行状态更新(例如在一个定时器中执行状态更新),这些状态更新不会被标记为 transition。

  • 标记为 transition 的状态更新将被其他状态更新打断。例如在 transition 中更新图表组件,并在图表组件仍在重新渲染时继续在输入框中输入,React 将首先处理输入框的更新,之后再重新启动对图表组件的渲染工作。

  • transition 更新不能用于控制文本输入。

  • 目前,React 会批处理多个同时进行的 transition。这是一个限制,可能会在未来版本中删除。


用法

将状态更新标记为非阻塞的 transition

在组件的顶层调用 useTransition 以将状态更新标记为非阻塞的 transition。

import { useState, useTransition } from 'react';

function TabContainer() {
const [isPending, startTransition] = useTransition();
// ……
}

useTransition 返回一个由两个元素组成的数组:

  1. isPending,告诉你是否存在待处理的 transition。
  2. startTransition 函数,你可以使用此方法将状态更新标记为 transition。

你可以按照以下方式将状态更新标记为 transition:

function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('about');

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

transition 可以使用户界面的更新在慢速设备上仍保持响应性。

通过 transition,UI 仍将在重新渲染过程中保持响应性。例如用户点击一个选项卡,但改变了主意并点击另一个选项卡,他们可以在不等待第一个重新渲染完成的情况下完成操作。

使用 useTransition 与常规状态更新的区别

1示例 2 个挑战:
在 transition 中更新当前选项卡

在此示例中,“文章”选项卡被 人为地减慢,以便至少需要一秒钟才能渲染。

点击“Posts”,然后立即点击“Contact”。请注意,这会中断“Posts”的缓慢渲染,而“联系人”选项卡将会立即显示。因为此状态更新被标记为 transition,所以缓慢的重新渲染不会冻结用户界面。

import { useState, useTransition } from 'react';
import TabButton from './TabButton.js';
import AboutTab from './AboutTab.js';
import PostsTab from './PostsTab.js';
import ContactTab from './ContactTab.js';

export default function TabContainer() {
  const [isPending, startTransition] = useTransition();
  const [tab, setTab] = useState('about');

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

  return (
    <>
      <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>
      <hr />
      {tab === 'about' && <AboutTab />}
      {tab === 'posts' && <PostsTab />}
      {tab === 'contact' && <ContactTab />}
    </>
  );
}


在 transition 中更新父组件

你也可以通过调用 useTransition 以更新父组件状态。例如,TabButton 组件在 transition 中包装了 onClick 逻辑:

export default function TabButton({ children, isActive, onClick }) {
const [isPending, startTransition] = useTransition();
if (isActive) {
return <b>{children}</b>
}
return (
<button onClick={() => {
startTransition(() => {
onClick();
});
}}>
{children}
</button>
);
}

由于父组件的状态更新在 onClick 事件处理程序内,所以该状态更新会被标记为 transition。这就是为什么可以在点击“Posts”后立即点击“Contact”。由于更新选定选项卡被标记为了 transition,因此它不会阻止用户交互。

import { useTransition } from 'react';

export default function TabButton({ children, isActive, onClick }) {
  const [isPending, startTransition] = useTransition();
  if (isActive) {
    return <b>{children}</b>
  }
  return (
    <button onClick={() => {
      startTransition(() => {
        onClick();
      });
    }}>
      {children}
    </button>
  );
}


在 transition 期间显示待处理的视觉状态

你可以使用 useTransition 返回的 isPending 布尔值来向用户表明当前处于 transition 中。例如,选项卡按钮可以有一个特殊的“pending”视觉状态:

function TabButton({ children, isActive, onClick }) {
const [isPending, startTransition] = useTransition();
// ...
if (isPending) {
return <b className="pending">{children}</b>;
}
// ...

请注意,现在点击“Posts”感觉更加灵敏,因为选项卡按钮本身立即更新了:

import { useTransition } from 'react';

export default function TabButton({ children, isActive, onClick }) {
  const [isPending, startTransition] = useTransition();
  if (isActive) {
    return <b>{children}</b>
  }
  if (isPending) {
    return <b className="pending">{children}</b>;
  }
  return (
    <button onClick={() => {
      startTransition(() => {
        onClick();
      });
    }}>
      {children}
    </button>
  );
}


避免不必要的加载指示器

在这个例子中,PostsTab 组件从启用了 Suspense 的数据源中获取了一些数据。当你点击“Posts”选项卡时,PostsTab 组件将 挂起,导致最近的加载占位符出现:

import { Suspense, useState } from 'react';
import TabButton from './TabButton.js';
import AboutTab from './AboutTab.js';
import PostsTab from './PostsTab.js';
import ContactTab from './ContactTab.js';

export default function TabContainer() {
  const [tab, setTab] = useState('about');
  return (
    <Suspense fallback={<h1>🌀 Loading...</h1>}>
      <TabButton
        isActive={tab === 'about'}
        onClick={() => setTab('about')}
      >
        About
      </TabButton>
      <TabButton
        isActive={tab === 'posts'}
        onClick={() => setTab('posts')}
      >
        Posts
      </TabButton>
      <TabButton
        isActive={tab === 'contact'}
        onClick={() => setTab('contact')}
      >
        Contact
      </TabButton>
      <hr />
      {tab === 'about' && <AboutTab />}
      {tab === 'posts' && <PostsTab />}
      {tab === 'contact' && <ContactTab />}
    </Suspense>
  );
}

隐藏整个选项卡容器以显示加载指示符会导致用户体验不连贯。如果你将 useTransition 添加到 TabButton 中,你可以改为在选项卡按钮中指示待处理状态。

请注意,现在点击“帖子”不再用一个旋转器替换整个选项卡容器:

import { useTransition } from 'react';

export default function TabButton({ children, isActive, onClick }) {
  const [isPending, startTransition] = useTransition();
  if (isActive) {
    return <b>{children}</b>
  }
  if (isPending) {
    return <b className="pending">{children}</b>;
  }
  return (
    <button onClick={() => {
      startTransition(() => {
        onClick();
      });
    }}>
      {children}
    </button>
  );
}

了解有关在Suspense中使用转换的更多信息

注意

转换效果只会“等待”足够长的时间来避免隐藏 已经显示 的内容(例如选项卡容器)。如果“帖子”选项卡具有一个嵌套 <Suspense> 边界,转换效果将不会“等待”它。


构建一个Suspense-enabled 的路由

如果你正在构建一个 React 框架或路由,我们建议将页面导航标记为转换效果。

function Router() {
const [page, setPage] = useState('/');
const [isPending, startTransition] = useTransition();

function navigate(url) {
startTransition(() => {
setPage(url);
});
}
// ...

这么做有两个好处:

下面是一个简单的使用转换效果进行页面导航的路由器示例:

import { Suspense, useState, useTransition } from 'react';
import IndexPage from './IndexPage.js';
import ArtistPage from './ArtistPage.js';
import Layout from './Layout.js';

export default function App() {
  return (
    <Suspense fallback={<BigSpinner />}>
      <Router />
    </Suspense>
  );
}

function Router() {
  const [page, setPage] = useState('/');
  const [isPending, startTransition] = useTransition();

  function navigate(url) {
    startTransition(() => {
      setPage(url);
    });
  }

  let content;
  if (page === '/') {
    content = (
      <IndexPage navigate={navigate} />
    );
  } else if (page === '/the-beatles') {
    content = (
      <ArtistPage
        artist={{
          id: 'the-beatles',
          name: 'The Beatles',
        }}
      />
    );
  }
  return (
    <Layout isPending={isPending}>
      {content}
    </Layout>
  );
}

function BigSpinner() {
  return <h2>🌀 Loading...</h2>;
}

注意

启用 Suspense 的路由默认情况下会将页面导航更新包装为 transition。


疑难解答

在 transition 中无法更新输入框内容

不应将控制输入框的状态变量标记为 transition:

const [text, setText] = useState('');
// ...
function handleChange(e) {
// ❌ 不应将受控输入框的状态变量标记为 transition
startTransition(() => {
setText(e.target.value);
});
}
// ...
return <input value={text} onChange={handleChange} />;

这是因为 transition 是非阻塞的,但是在响应更改事件时更新输入应该是同步的。如果想在输入时运行一个 transition,那么有两种做法:

  1. 声明两个独立的状态变量:一个用于输入状态(它总是同步更新),另一个用于在 transition 中更新。这样,便可以使用同步状态控制输入,并将用于 transition 的状态变量(它将“滞后”于输入)传递给其余的渲染逻辑。
  2. 或者使用一个状态变量,并添加 useDeferredValue,它将“滞后”于实际值,并自动触发非阻塞的重新渲染以“追赶”新值。

React 没有将状态更新视为 transition

当在 transition 中包装状态更新时,请确保它发生在 startTransition 调用期间:

startTransition(() => {
// ✅ 在调用 startTransition 中更新状态
setPage('/about');
});

传递给 startTransition 的函数必须是同步的。

你不能像这样将更新标记为 transition:

startTransition(() => {
// ❌ 在调用 startTransition 后更新状态
setTimeout(() => {
setPage('/about');
}, 1000);
});

相反,你可以这样做:

setTimeout(() => {
startTransition(() => {
// ✅ 在调用 startTransition 中更新状态
setPage('/about');
});
}, 1000);

类似地,你不能像这样将更新标记为 transition:

startTransition(async () => {
await someAsyncFunction();
// ❌ 在调用 startTransition 后更新状态
setPage('/about');
});

然而,使用以下方法可以正常工作:

await someAsyncFunction();
startTransition(() => {
// ✅ 在调用 startTransition 中更新状态
setPage('/about');
});

我想在组件外部调用 useTransition

useTransition 是一个 Hook,因此不能在组件外部调用。请使用独立的 startTransition 方法。它们的工作方式相同,但不提供 isPending 标记。


我传递给 startTransition 的函数会立即执行

如果你运行这段代码,它将会打印 1, 2, 3:

console.log(1);
startTransition(() => {
console.log(2);
setPage('/about');
});
console.log(3);

期望打印 1, 2, 3。传递给 startTransition 的函数不会被延迟执行。与浏览器的 setTimeout 不同,它不会延迟执行回调。React 会立即执行你的函数,但是在它运行的同时安排的任何状态更新都被标记为 transition。你可以将其想象为以下方式:

// React 运行的简易版本

let isInsideTransition = false;

function startTransition(scope) {
isInsideTransition = true;
scope();
isInsideTransition = false;
}

function setState() {
if (isInsideTransition) {
// ……安排 transition 状态更新……
} else {
// ……安排紧急状态更新……
}
}