useTransition
是一個 React Hook,可讓您在背景中渲染部分 UI。
const [isPending, startTransition] = useTransition()
參考
useTransition()
在元件的頂層呼叫 useTransition
將某些狀態更新標記為 Transitions。
import { useTransition } from 'react';
function TabContainer() {
const [isPending, startTransition] = useTransition();
// ...
}
參數
useTransition
不接受任何參數。
回傳值
useTransition
會回傳一個剛好包含兩個項目的陣列
isPending
旗標,告知您是否有擱置中的 Transition。startTransition
函式,可讓您將更新標記為 Transition。
startTransition(action)
useTransition
回傳的 startTransition
函式可讓您將更新標記為 Transition。
function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('about');
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
// ...
}
參數
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
會回傳一個剛好包含兩個項目的陣列
- `isPending` 標記(
isPending
flag)會告訴您是否有正在處理中的 Transition(轉場)。 - `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 進行中時提供即時回饋。
範例 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> ); }
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 的路由器
如果您正在建構 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>; }
使用錯誤邊界向使用者顯示錯誤
如果傳遞給 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} />;
這是因為轉場是非阻塞的,但是響應更改事件更新輸入應該是同步發生的。如果您想響應輸入而執行轉場,則有兩個選項:
- 您可以宣告兩個單獨的狀態變數:一個用於輸入狀態(始終同步更新),另一個將在轉場中更新。這讓您可以使用同步狀態控制輸入,並將轉場狀態變數(將「滯後」於輸入)傳遞給其餘的渲染邏輯。
- 或者,您可以擁有一個狀態變數,並新增
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>
動作,可以為您處理排序。對於進階的使用案例,您需要自行實作佇列和中止邏輯來處理此問題。