React19で新しくなったuseTransition
はじめに
現在、React19 の RC 版がリリースされています。
React19 では様々な新機能が発表されていますが、その中でも新しくなったuseTransitionについて紹介してきます。
本記事では、
-
useTransitionを使わない場合 - React18 までの
useTransitionを使った場合 - React19 で新しくなった
useTransitionを使った場合
の 3 つを比較して説明していきます。
useTransition とは
useTransition は、React18 で導入されました。
useTransition は公式ドキュメントには「UI をブロックせずに state を更新するための React フック」と表現されています。ざっくりと「state 更新の優先度を下げるためのフック」と言い換えられるのではないかと考えられます。
ではuseTransitionを使わず state を更新するやり方と、useTransitionを使って state を更新するやり方を比較してみます。
useTransition を使わずに state を更新する
例として、タブでページ遷移するアプリケーションを考えていきます。
タブをクリックするとそのページに遷移して、選択中のタブが太字になるようなイメージです。

やり方としては現在のページ数をcurrentPageで保持して、タブがクリックされたときにchangePage関数が発火してsetCurrentPageでcurrentPageが更新されます。
以下がコードになります。また本記事ではコード上では CSS は省略しています。
const [currentPage, setCurrentPage] = useState(1);
const changePage = (newPage: number) => {
setCurrentPage(newPage);
};
return (
<div>
<div>
<button onClick={() => changePage(1)}>ページ1</button>
<button onClick={() => changePage(2)}>ページ2</button>
<button onClick={() => changePage(3)}>ページ3</button>
</div>
<div>
{currentPage === 1 && <p>現在のページは1</p>}
{currentPage === 2 && <p>現在のページは2</p>}
{currentPage === 3 && <p>現在のページは3</p>}
</div>
</div>
);
ここで問題になるのが、とあるページの読み込みに時間がかかり state 更新がとても重い処理だった場合、state の更新中にユーザーは他の操作をすることができず、待機状態を UI 上で把握することができないことです。
以下のコードで、ページ 3 がクリックされたときに表示する<SlowPage>のように読み込みに時間がかかる場合があります。
const [currentPage, setCurrentPage] = useState(1);
const changePage = (newPage: number) => {
setCurrentPage(newPage);
};
return (
<div>
<div>
<button onClick={() => changePage(1)}>ページ1</button>
<button onClick={() => changePage(2)}>ページ2</button>
<button onClick={() => changePage(3)}>ページ3(激重)</button>
</div>
<div>
{currentPage === 1 && <p>現在のページは1</p>}
{currentPage === 2 && <p>現在のページは2</p>}
{/* ⏬ SlowPageは表示に時間がかかるコンポーネント */}
{currentPage === 3 && <SlowPage />}
</div>
</div>
);

こちらの Gif 画像だと分かりにくいのですが、ページ3(激重)のタブをクリックしたときに表示するページのレンダリングに時間がかかっておりその間、UI は他のタブをクリックしても反応しなくなります。そのためページ3を読み込んでいるときにユーザーがページ3を見ることをやめ、他のページのタブをクリックできないようになってしまっています。
公式のデモで同じような挙動を体験することができます。(デモ)
useTransition を使って state を更新する
useTransitionを使うことで、UI をブロックせずに state を更新することができます。
使い方としてはuseTransitionを呼び出し、その返り値としてisPending、startTransitionを受け取ります。startTransitionで state 更新の set 関数をくくることで、UI をブロックせずに優先度を下げて state を更新することができます。
このように優先度を下げて state 更新することを公式ドキュメントでは「トランジション」というように表現されているようです。
ちなみにisPendingとstartTransitionの命名は慣例的なもので、他の命名でも可能です。
const [isPending, startTransition] = useTransition();
startTransition(() => {
setFoo(foo);
});
ではタブでページ遷移するアプリケーションの例に戻ります。
先ほど UI をブロックしていたsetCurrentPageにstartTransitionをくくります。
const [currentPage, setCurrentPage] = useState(1);
+ const [isPending, startTransition] = useTransition();
const changePage = (newPage: number) => {
+ startTransition(() => {
setCurrentPage(newPage);
+ });
};
return (
<div>
<div>
<button onClick={() => changePage(1)}>ページ1</button>
<button onClick={() => changePage(2)}>ページ2</button>
<button onClick={() => changePage(3)}>ページ3(激重)</button>
</div>
<div>
{currentPage === 1 && <FastPage pageNumber={1} />}
{currentPage === 2 && <FastPage pageNumber={2} />}
{/* ⏬ SlowPageは表示に時間がかかるコンポーネント */}
{currentPage === 3 && <SlowPage />}
</div>
</div>
);
また、startTransitionでくくった処理が完了したかどうかはisPendingに格納されます。
そのため以下のように裏でページの読み込み、state 更新が実行されていることをisPendingを使って表現することができます。
// ページ3取得中のときに、ボタンを少し透明にする
<button
onClick={() => changePage(3)}
+ className={isPending ? "opacity-50" : ""}
>
ページ3(激重)
</button>

ページ3のタブをクリックしてページ3取得中でも、UI はブロックされず、ページ1やページ2に切り替えることができています。このようにuseTransitionを使うことで、UI をブロックせずに state を更新できるようになります。
ここまでが React18 までのuseTransitionです。
React19 で新しくなった useTransition
React19 では従来の useTransition と比較して、非同期関数を扱えるようになりました。
つまり、以下のように startTransition で非同期関数をくくれるようになりました。
startTransition(async () => {
const foo = await getFoo(); // 非同期関数が扱える
setFoo(foo);
});
では実際に例を見てみましょう。
今度は、input 要素にユーザー ID を入力し検索ボタンをクリックしたときに、ユーザー ID に紐づくユーザーの情報を表示するアプリケーションを例に考えていきます。先ほどのisPendingのように検索中は、「読み込み中...」の文字を表示するようにします。

従来の実装
const [userId, setUserId] = useState("");
const [user, setUser] = useState<User | null>(null);
const [isPending, setIsPending] = useState(false);
const handleClick = async () => {
setUser(null);
setIsPending(true); // isPendingを手動で切り替える
const user = await getUser(userId);
setUser(user);
setIsPending(false);
};
return (
<div>
<div>
<input type="number" onChange={(e) => setUserId(e.target.value)} />
<button onClick={handleClick}>検索</button>
</div>
{isPending && <p>読み込み中...</p>}
{user && (
<div>
<p>名前: {user.name}</p>
<p>メール: {user.email}</p>
<p>電話番号: {user.phone}</p>
<p>ウェブサイト: {user.website}</p>
<p>会社: {user.company.name}</p>
</div>
)}
</div>
);
検索ボタンをクリックしたときに handleClick関数でユーザーの取得を行っています。
こちらではisPending を手動で切り替えています。
useTransition を使った実装
const [userId, setUserId] = useState("");
const [user, setUser] = useState<User | null>(null);
+ const [isPending, startTransition] = useTransition();
const handleClick = () => {
setUser(null);
+ startTransition(async () => {
const user = await getUser(userId);
setUser(user);
+ });
};
return (
<div>
<div>
<input type="number" onChange={(e) => setUserId(e.target.value)} />
<button onClick={handleClick}>検索</button>
</div>
{isPending && <p>読み込み中...</p>}
{user && (
<div>
<p>名前: {user.name}</p>
<p>メール: {user.email}</p>
<p>電話番号: {user.phone}</p>
<p>ウェブサイト: {user.website}</p>
<p>会社: {user.company.name}</p>
</div>
)}
</div>
);
従来の実装と比較して、非同期関数の getUser と set 関数を startTransition でくくっています。
このように非同期関数を startTransition 内で実行すること UI をブロックせずに state を更新できるだけでなく、isPending の管理を useTransition 側に委ねることができるので、より簡潔に書くことができると感じます。
最後に
React19 では、useTransition が非同期関数を扱えるようになったことで、React18 以前よりもスムーズな UI が実現しやすくなっていると感じました。また、ローディング状態を手動でトグルする必要がなくなったことでコードがよりシンプルになったと感じます。
一方でこれまでクライアントフェッチは SWR や TanStack Query のようなライブラリを使ったフェッチがよく行われてきました。そこでこれらのフェッチライブラリと今回の例のような非同期処理を扱う useTransition をどう使い分けていくかが個人的にはまだ明確でないです。
また React19 で出るuseActionStateを使うことでuseTransitionの恩恵を受けつつ、state の管理が不要になります。そのため、state の管理が必要な非同期処理の実行にはuseActionStateが使われていくものと考えられます。
そこでミニマムにuseTransitionを使う場面と包括的なuseActionStateを使う場面をどう使う分けていくか、今後勉強していき理解を深めていきたいです。
Discussion