簡介
在用了 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 筆資料,我們可以將 onClick
用 startTransition
標記它為較低優先的更新,並且用 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>
);
}
案例 2. Server Action
在用 app router 時經常會用到 server action 這個功能,而 server action 顧名思義是在 server 執行的,要如何判斷它是否已經執行完了呢?
這時候 useTransition
同樣可以派上用場,在以下的範例中 updateResume
是一個 server action,我們可以用 startTransition
將 updateResume
包起來,並且使用 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
是否已經結束,可以使用 startTransition
將 router.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
中可以使用 useTransition
的 isPending
來判斷是否 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 擁有非常強大的功能,適用於多種情境。如果你發現了更多的使用案例或新的創意用法,歡迎分享!!
Reference
- https://react.dev/reference/react/useTransition
- https://react.dev/reference/react/useActionState
- https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#server-side-form-validation
- https://github.com/orgs/react-hook-form/discussions/10757
- https://github.com/vercel/next.js/discussions/54157