useTransition

useTransition 是一個 React Hook,可讓您在背景中渲染部分 UI。

const [isPending, startTransition] = useTransition()

參考

useTransition()

在元件的頂層呼叫 useTransition 將某些狀態更新標記為 Transitions。

import { useTransition } from 'react';

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

請參閱下面的更多範例。

參數

useTransition 不接受任何參數。

回傳值

useTransition 會回傳一個剛好包含兩個項目的陣列

  1. isPending 旗標,告知您是否有擱置中的 Transition。
  2. startTransition 函式,可讓您將更新標記為 Transition。

startTransition(action)

useTransition 回傳的 startTransition 函式可讓您將更新標記為 Transition。

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

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

注意

startTransition 中呼叫的函式稱為「動作 (Actions)」。

傳遞給 startTransition 的函式稱為「動作 (Action)」。 慣例上,任何在 startTransition 內部呼叫的回呼函式 (例如回呼屬性) 應命名為 action 或包含「Action」後綴。

function SubmitButton({ submitAction }) {
const [isPending, startTransition] = useTransition();

return (
<button
disabled={isPending}
onClick={() => {
startTransition(() => {
submitAction();
});
}}
>
Submit
</button>
);
}

參數

  • action:一個透過呼叫一個或多個 set 函式 來更新狀態的函式。React 會立即以無參數方式呼叫 action,並將在 action 函式呼叫期間同步排程的所有狀態更新標記為轉換 (Transitions)。任何在 action 中被 await 的非同步呼叫都將包含在轉換中,但目前需要將 await 之後的任何 set 函式包裝在另一個 startTransition 中 (請參閱疑難排解)。標記為轉換的狀態更新將會是 非阻塞的 (non-blocking),並且 不會顯示不需要的載入指示器

回傳值

startTransition 不回傳任何值。

注意事項

  • useTransition 是一個 Hook,因此它只能在元件或自定義 Hook 內部呼叫。如果您需要在其他地方啟動轉換 (例如,從資料庫),請改為呼叫獨立的 startTransition

  • 只有當您可以存取該狀態的 set 函式時,才能將更新包裝到轉換中。如果您想響應某些屬性或自定義 Hook 值來啟動轉換,請嘗試使用 useDeferredValue

  • 傳遞給 startTransition 的函式會立即被呼叫,將其執行期間發生的所有狀態更新標記為轉換。例如,如果您嘗試在 setTimeout 中執行狀態更新,它們將不會被標記為轉換。

  • 您必須將任何非同步請求之後的狀態更新包裝在另一個 startTransition 中,才能將它們標記為轉換。這是一個已知的限制,我們將在未來修復它 (請參閱疑難排解)。

  • startTransition 函式具有穩定的識別身分 (identity),因此您經常會看到它從 Effect 相依性中被省略,但包含它不會導致 Effect 觸發。如果程式碼檢查工具 (linter) 允許您在沒有錯誤的情況下省略相依性,則可以安全地執行此操作。深入瞭解移除 Effect 相依性。

  • 標記為轉換的狀態更新將會被其他狀態更新中斷。例如,如果您在轉換內更新圖表元件,但隨後在圖表重新渲染過程中開始在輸入框中輸入文字,React 將在處理輸入更新後重新啟動圖表元件的渲染工作。

  • 轉換更新不能用於控制文字輸入。

  • 如果有多個正在進行的轉換,React 目前會將它們批次處理在一起。這是一個限制,可能會在未來的版本中移除。

用法

使用動作執行非阻塞更新

在元件頂部呼叫 useTransition 以建立動作,並存取擱置狀態

import {useState, useTransition} from 'react';

function CheckoutForm() {
const [isPending, startTransition] = useTransition();
// ...
}

useTransition 會回傳一個剛好包含兩個項目的陣列

  1. `isPending` 標記(isPending flag)會告訴您是否有正在處理中的 Transition(轉場)。
  2. `startTransition` 函式(startTransition function)讓您可以建立一個 Action(動作)。

要開始一個 Transition,請將一個函式傳遞給 startTransition,如下所示:

import {useState, useTransition} from 'react';
import {updateQuantity} from './api';

function CheckoutForm() {
const [isPending, startTransition] = useTransition();
const [quantity, setQuantity] = useState(1);

function onSubmit(newQuantity) {
startTransition(async function () {
const savedQuantity = await updateQuantity(newQuantity);
startTransition(() => {
setQuantity(savedQuantity);
});
});
}
// ...
}

傳遞給 startTransition 的函式稱為「Action」。您可以在 Action 中更新狀態並(選擇性地)執行副作用,這些工作將在背景中執行,而不會阻擋頁面上使用者的互動。一個 Transition 可以包含多個 Action,並且在 Transition 進行中時,您的 UI 保持回應。例如,如果使用者點擊一個分頁,但隨後改變主意並點擊另一個分頁,則會立即處理第二次點擊,而無需等待第一次更新完成。

為了向使用者提供有關進行中 Transition 的回饋,isPending 狀態在第一次呼叫 startTransition 時會切換為 true,並保持 true,直到所有 Action 完成,並且最終狀態顯示給使用者。Transition 確保 Action 中的副作用按順序完成,以防止不必要的載入指示器,並且您可以使用 useOptimistic 在 Transition 進行中時提供即時回饋。

Action 和一般事件處理的差異

範例 1說明 2:
在 Action 中更新數量

在此範例中,updateQuantity 函式模擬向伺服器發出請求以更新購物車中商品的數量。此函式被人為地減慢速度,因此完成請求至少需要一秒鐘。

快速多次更新數量。請注意,在任何請求進行中時,都會顯示待處理的「總計」狀態,並且僅在最終請求完成後才會更新「總計」。因為更新在 Action 中,所以可以在請求進行中時繼續更新「數量」。

import { useState, useTransition } from "react";
import { updateQuantity } from "./api";
import Item from "./Item";
import Total from "./Total";

export default function App({}) {
  const [quantity, setQuantity] = useState(1);
  const [isPending, startTransition] = useTransition();

  const updateQuantityAction = async newQuantity => {
    // To access the pending state of a transition,
    // call startTransition again.
    startTransition(async () => {
      const savedQuantity = await updateQuantity(newQuantity);
      startTransition(() => {
        setQuantity(savedQuantity);
      });
    });
  };

  return (
    <div>
      <h1>Checkout</h1>
      <Item action={updateQuantityAction}/>
      <hr />
      <Total quantity={quantity} isPending={isPending} />
    </div>
  );
}

這是一個基本範例,用於說明 Action 的工作原理,但此範例未處理請求以錯誤順序完成的情況。多次更新數量時,先前的請求可能會在後續請求之後完成,導致數量更新順序錯誤。這是一個已知的限制,我們將來會修復(請參閱下面的疑難排解)。

對於常見的使用案例,React 提供內建的抽象概念,例如:

這些解決方案會為您處理請求順序。當使用 Transition 建立您自己的自定義 hooks 或函式庫來管理非同步狀態轉換時,您可以更好地控制請求順序,但您必須自行處理。


從元件公開 action 屬性

您可以從元件公開一個 action 屬性,以允許父元件呼叫 Action。

例如,這個 TabButton 元件將其 onClick 邏輯包裝在一個 action 屬性中

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

因為父元件在 action 內部更新其狀態,所以該狀態更新會被標記為 Transition。這表示您可以點擊「貼文」,然後立即點擊「聯絡人」,它不會阻擋使用者互動

import { useTransition } from 'react';

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


顯示待處理的視覺狀態

您可以使用 useTransition 返回的布林值 isPending 向使用者指示 Transition 正在進行中。例如,分頁按鈕可以具有特殊的「待處理」視覺狀態

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

請注意,點擊「貼文」現在感覺更靈敏,因為分頁按鈕本身會立即更新

import { useTransition } from 'react';

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


防止不必要的載入指示器

在此範例中,PostsTab 元件使用 use 來擷取一些資料。當您點擊「貼文」分頁時,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'}
        action={() => setTab('about')}
      >
        About
      </TabButton>
      <TabButton
        isActive={tab === 'posts'}
        action={() => setTab('posts')}
      >
        Posts
      </TabButton>
      <TabButton
        isActive={tab === 'contact'}
        action={() => 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({ action, children, isActive }) {
  const [isPending, startTransition] = useTransition();
  if (isActive) {
    return <b>{children}</b>
  }
  if (isPending) {
    return <b className="pending">{children}</b>;
  }
  return (
    <button onClick={() => {
      startTransition(() => {
        action();
      });
    }}>
      {children}
    </button>
  );
}

閱讀更多關於將轉場與 Suspense 搭配使用的資訊。

注意

轉場只會「等待」足夠的時間,以避免隱藏 _已顯示_ 的內容(例如分頁容器)。如果「貼文」分頁具有 巢狀 <Suspense> 邊界,則轉場將不會「等待」它。


建構支援 Suspense 的路由器

如果您正在建構 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 的 路由器預計會將導覽更新預設包裝到轉場中。


使用錯誤邊界向使用者顯示錯誤

如果傳遞給 startTransition 的函式拋出錯誤,您可以使用 錯誤邊界 向使用者顯示錯誤。要使用錯誤邊界,請將呼叫 useTransition 的元件包裝在錯誤邊界中。一旦傳遞給 startTransition 的函式發生錯誤,就會顯示錯誤邊界的後備方案。

import { useTransition } from "react";
import { ErrorBoundary } from "react-error-boundary";

export function AddCommentContainer() {
  return (
    <ErrorBoundary fallback={<p>⚠️Something went wrong</p>}>
      <AddCommentButton />
    </ErrorBoundary>
  );
}

function addComment(comment) {
  // For demonstration purposes to show Error Boundary
  if (comment == null) {
    throw new Error("Example Error: An error thrown to trigger error boundary");
  }
}

function AddCommentButton() {
  const [pending, startTransition] = useTransition();

  return (
    <button
      disabled={pending}
      onClick={() => {
        startTransition(() => {
          // Intentionally not passing a comment
          // so error gets thrown
          addComment();
        });
      }}
    >
      Add comment
    </button>
  );
}


疑難排解

在轉場中更新輸入無效

您無法將轉場用於控制輸入的狀態變數。

const [text, setText] = useState('');
// ...
function handleChange(e) {
// ❌ Can't use Transitions for controlled input state
startTransition(() => {
setText(e.target.value);
});
}
// ...
return <input value={text} onChange={handleChange} />;

這是因為轉場是非阻塞的,但是響應更改事件更新輸入應該是同步發生的。如果您想響應輸入而執行轉場,則有兩個選項:

  1. 您可以宣告兩個單獨的狀態變數:一個用於輸入狀態(始終同步更新),另一個將在轉場中更新。這讓您可以使用同步狀態控制輸入,並將轉場狀態變數(將「滯後」於輸入)傳遞給其餘的渲染邏輯。
  2. 或者,您可以擁有一個狀態變數,並新增 useDeferredValue,它將「滯後」於實際值。它會觸發非阻塞的重新渲染以自動「趕上」新值。

React 未將我的狀態更新視為轉場

當您將狀態更新包裝在轉場中時,請確保它發生在 startTransition 呼叫 _期間_。

startTransition(() => {
// ✅ Setting state *during* startTransition call
setPage('/about');
});

您傳遞給 startTransition 的函式必須是同步的。您無法像這樣將更新標記為轉場:

startTransition(() => {
// ❌ Setting state *after* startTransition call
setTimeout(() => {
setPage('/about');
}, 1000);
});

相反,您可以這樣做:

setTimeout(() => {
startTransition(() => {
// ✅ Setting state *during* startTransition call
setPage('/about');
});
}, 1000);

React 並不會將我在 await 之後的狀態更新視為 Transition

當您在 startTransition 函式內使用 await 時,await 之後的狀態更新不會被標記為 Transition。您必須將每個 await 之後的狀態更新都包裝在 startTransition 呼叫中。

startTransition(async () => {
await someAsyncFunction();
// ❌ Not using startTransition after await
setPage('/about');
});

然而,這樣做可以解決問題

startTransition(async () => {
await someAsyncFunction();
// ✅ Using startTransition *after* await
startTransition(() => {
setPage('/about');
});
});

這是由於 React 失去了非同步上下文的作用範圍,這是 JavaScript 的限制。未來,當 AsyncContext 可用時,此限制將被移除。


我想從元件外部呼叫 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。您可以想像它的工作方式如下

// A simplified version of how React works

let isInsideTransition = false;

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

function setState() {
if (isInsideTransition) {
// ... schedule a Transition state update ...
} else {
// ... schedule an urgent state update ...
}
}

我的 Transition 中的狀態更新順序錯亂了

如果您在 startTransition 內使用 await,您可能會看到更新的順序錯亂。

在此範例中,updateQuantity 函式模擬向伺服器發出請求以更新購物車中商品數量的動作。此函式*刻意在先前的請求之後返回每隔一個請求*,以模擬網路請求的競爭條件。

嘗試更新一次數量,然後快速多次更新它。您可能會看到不正確的總數

import { useState, useTransition } from "react";
import { updateQuantity } from "./api";
import Item from "./Item";
import Total from "./Total";

export default function App({}) {
  const [quantity, setQuantity] = useState(1);
  const [isPending, startTransition] = useTransition();
  // Store the actual quantity in separate state to show the mismatch.
  const [clientQuantity, setClientQuantity] = useState(1);
  
  const updateQuantityAction = newQuantity => {
    setClientQuantity(newQuantity);

    // Access the pending state of the transition,
    // by wrapping in startTransition again.
    startTransition(async () => {
      const savedQuantity = await updateQuantity(newQuantity);
      startTransition(() => {
        setQuantity(savedQuantity);
      });
    });
  };

  return (
    <div>
      <h1>Checkout</h1>
      <Item action={updateQuantityAction}/>
      <hr />
      <Total clientQuantity={clientQuantity} savedQuantity={quantity} isPending={isPending} />
    </div>
  );
}

當多次點擊時,先前的請求有可能在後面的請求之後完成。當發生這種情況時,React 目前無法知道預期的順序。這是因為更新是非同步排定的,React 在非同步邊界上失去了順序的上下文。

這是預期的,因為 Transition 內的動作不保證執行順序。對於常見的使用案例,React 提供了更高級別的抽象,例如 useActionState<form> 動作,可以為您處理排序。對於進階的使用案例,您需要自行實作佇列和中止邏輯來處理此問題。