在 Next.js 中,何時應該使用 useTransition()

useTransition() 的三種常見應用場景

Leo Chiu
11 min readOct 26, 2024

簡介

在用了 Next.js 的 app router 後,覺得有些情境很適合使用 useTransition 這個 React hook,所以整理一篇文章來闡述 useTransition 的使用時機。

在了解 useTransition 之前,我們要先知道 React 18 一個很重要的觀念是 concurrent,它讓處理大量狀態更新的時候會分高低優先級來處理,較不緊急的更新將會在背景執行,不會打斷較高優先級的更新。

useTransition 即是在 concurrent 下衍生的一個 React hook,它可以被用來標記較低優先級,而不阻塞較高優先級的更新。

例如以下範例中,我們判斷 setTab 是一個較低優先的操作,所以可以使用 startTransition()setTab 包起來,它將會避免阻塞渲染。

import { useTransition } from 'react';

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

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

isPending 則是可以用來判斷被 startTransition() 包起來的操作是否已經執行完,它可以被用在很多地方來優化 UX。

案例 1. 大量的狀態更新導致渲染阻塞

這個案例不只是在 Next.js 中會遇到,只要是寫 React 都可能會遇到類似的案例。假設有個 Posts tab,這個 Posts tab 進入後會載入 10000 筆的資料,在還沒實作之前大概就會預期它會使用到大量的渲染資源。

通常優化方式不外乎是 pagination、 virtual list 等等的策略,避免畫面一次出現太多的元素。但如果它不是可以被 pagination 的資料呢?或是它是複雜的狀態計算呢?例如在客戶端處理 10000 筆資料的過濾。

這時候 useTransition 就非常有用,我們可以使用 transition 讓狀態更新在背景執行,並且搭配 isPending 來顯示 spinner 或是改變一些樣式來優化 UX。

以下範例中點擊了 Posts tab 即會載入 10000 筆資料,我們可以將 onClickstartTransition 標記它為較低優先的更新,並且用 isPending 來顯示不同的樣式:

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>
);
}
https://react.dev/reference/react/useTransition#usetransition

案例 2. Server Action

在用 app router 時經常會用到 server action 這個功能,而 server action 顧名思義是在 server 執行的,要如何判斷它是否已經執行完了呢?

這時候 useTransition 同樣可以派上用場,在以下的範例中 updateResume 是一個 server action,我們可以用 startTransitionupdateResume 包起來,並且使用 isPending 判斷要不要顯示 Spinner,這即是一個在使用 app router 時經常使用的 pattern。

import { useTransition } from 'react' 

const ResumeForm = () => {
const [isPending, startTransition] = useTransition()

const submitAction = async () => {
const valid = await trigger()
if (valid) {
startTransition(() => {
updateResume(resume.id, getValues())
})
}
}

return (
<>
{isPending && <Spinner />}
<form action={submitAction}>
// ...
)
}

useActionState

如果使用 form 搭配 server action,則有另外一個相對應的 hook — useActionState ,會比 useTransition 更直接對應 server action,但這種情況通常就必須使用 server-side validation。

import { useActionState } from 'react';
import { action } from './actions.js';

function MyComponent() {
const [state, formAction] = useActionState(action, null);
// ...
return (
<form action={formAction}>
{/* ... */}
</form>
);
}

但在寫 React 時我們經常會用一些管理 form 的套件,例如 ant design 的 form 或 react-hook-form 等等的方案,你想要善用套件的表單驗證,這時候採用 useTransition 的彈性更高。

const submitAction = async () => {
const valid = await trigger()
if (valid) {
startTransition(() => {
updateResume(resume.id, getValues())
})
}
}

而如果 server action 是像加入購物車這種功能,不需要在客戶端驗證,你可以選擇 useActionState 來判斷目前的狀態,寫起來會更乾淨。

題外話,React 19 將 useFormState 改名成 useActionState ,如果你在網路上看到 useFormState ,基本上都是指 useActionState

案例 3. navigation

Next.js 有自己的 router,我們經常會使用 useRouter 或是 usePathname 來實作客製化的路由。有時候我們想要知道「路由結束了嗎」,然後再做下一步的操作。例如以下兩個案例:

  • 輸入搜尋關鍵字時會轉址,並且你想要在搜尋匡加入 loading 的效果
  • 轉址時出現在瀏覽器視窗頂部的進度條

以下這個範例,當我們想要判斷 router.push 是否已經結束,可以使用 startTransitionrouter.push 包起來 。當然,你在 layout.tsx 中比較有可能用到這種的判斷方式。

// layout.tsx

import { useRouter } from "next/navigation";
import { useTransition } from "react";

const Layout = () => {
const router = useRouter();
const [isPending, startTransition] = useTransition();

const onClick = () => {
startTransition(() => {
router.push('/')
});
}

return (
<>
{isPending && "isPending"}
<button onClick={onClick}>back</button>
</>
);
}

MPA (Multi-Page Application) 的 view transition

隨著 View Transition API 支援度越來越好,也許在實作過渡動畫,View Transition API 變成一個熱門的選擇。

而要在 Next.js 實作 MPA 的 view transition 就會用到 useTransition,以下是客製化的 <Link> 的程式碼片段,document.startViewTransition 可以接收一個 promise 來判斷何時應該執行過渡。

const Link = ({ href }) => {
const router = useRouter();
const { startTransition, setFinishViewTransition } =
useViewTransitionContext();

const triggerTransition = useCallback(
() => {
if ("startViewTransition" in document) {
document.startViewTransition(
() =>
new Promise<void>((resolve) => {
startTransition(() => {
router.push(href);
setFinishViewTransition(() => resolve);
});
})
);
} else {
return cb();
}
},
[setFinishViewTransition, startTransition, router]
);

// ...
}

並且在 layout.tsx 中可以使用 useTransitionisPending 來判斷是否 router.push 已經執行完,執行完時 resolve,並且觸發 view transition。


export function ViewTransitions({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const [finishViewTransition, setFinishViewTransition] = useState(null);
const [isPending, startTransition] = useTransition();

useEffect(() => {
if (finishViewTransition && !isPending) {
finishViewTransition();
setFinishViewTransition(null);
}
}, [finishViewTransition, isPending]);

return (
<ViewTransitionsContext.Provider
value={{ startTransition, setFinishViewTransition }}
>
{children}
</ViewTransitionsContext.Provider>
);
}

Demo: https://next-js-view-transition-example.vercel.app/

GitHub repo: https://github.com/leochiu-a/Next.js-View-Transition-Example

總結

在這篇文章中,我們瞭解了三個 Next.js 搭配 useTransition 的使用者案例,分別是 1️⃣ 解決大量狀態更新造成的渲染阻塞、 2️⃣ server action 的執行狀態、 3️⃣ navigation。

這個 API 擁有非常強大的功能,適用於多種情境。如果你發現了更多的使用案例或新的創意用法,歡迎分享!!

--

--

Leo Chiu
Leo Chiu

Written by Leo Chiu

每天進步一點點,在終點遇見更好的自己。 Instragram 小帳:@leo.web.dev

No responses yet