useTransition は state 更新を緊急ではないものとして扱う Hook です。入力値の表示はすぐ更新し、重い検索結果やタブ内容の更新は後回しにすることで、操作への反応を保ちます。
更新の優先度を分ける
import { memo, useState, useTransition } from 'react';
type Item = { id: string; name: string };
const Results = memo(function Results({ items, query }: { items: Item[]; query: string }) {
const matches = items.filter((item) => item.name.includes(query));
return <ul>{matches.map((item) => <li key={item.id}>{item.name}</li>)}</ul>;
});
function Search({ items }: { items: Item[] }) {
const [query, setQuery] = useState('');
const [resultsQuery, setResultsQuery] = useState('');
const [isPending, startTransition] = useTransition();
function handleChange(value: string) {
setQuery(value);
startTransition(() => setResultsQuery(value));
}
return (
<>
<input value={query} onChange={(event) => handleChange(event.target.value)} />
{isPending && <p>結果を更新中</p>}
<Results items={items} query={resultsQuery} />
</>
);
}
検索語 query は controlled input の値なので直ちに更新します。一方、結果一覧に渡す resultsQuery の更新を transition にします。Results の絞り込みはレンダー中に行われ、その描画は新しい入力で中断できます。memo により、入力欄だけが先に更新されるレンダーでは、同じ props の Results を再描画しません。
startTransition の関数自体はその場で実行されます。後回しになるのは関数内で予約した state 更新と、それに伴うレンダーです。新しい入力があれば、進行中の結果一覧の描画は中断され、最新の検索語でやり直せます。重い絞り込みをイベントハンドラ内で実行すると入力をブロックするため、この例では Results のレンダー内に置いています。
その結果、isPending は transition による描画が完了するまで true になります。ローディング表示を出すだけでなく、古い検索結果を薄く表示するなど「更新中だが操作できる」ことを伝える UI に使えます。transition の中の更新は同期的に処理時間が短くなるわけではありません。
使わない場面
テキスト入力そのもの、チェックボックス、選択状態のように即座に反映すべき制御値を transition に入れてはいけません。通信の待機状態を作るだけの API でもないため、データ取得の仕組みやエラー処理は別途設計します。
非同期処理を transition 内で await した後の state 更新も transition として扱いたい場合は、現行 React ではもう一度 startTransition で包む必要があります。どの更新をすぐ反映し、どれを中断可能にするかを、ユーザー操作の観点から分けることが重要です。
pending UI の設計
isPending が true の間に画面全体を隠すと、transition の利点が失われます。前の結果を残して「更新中」を添える、押されたタブだけを軽く強調する、といった継続性のある表示が向いています。Suspense を伴う更新では、既存のコンテンツを保つ境界設計も重要になります。
まとめ
useTransition は処理時間を短縮するのではなく、更新の優先度を伝える Hook です。ユーザー操作を妨げる重い描画だけを transition にします。