useTransition は UI を部分的にバックグラウンドでレンダーするための React フックです。ユーザー操作をブロックせずに、高コストな state 更新を遅延させ、優れた UX を提供します。
const [isPending, startTransition] = useTransition()
1. なぜ useTransition が必要か
1.1 UI のブロッキング問題
React では、state 更新がトリガされると即座に再レンダーが開始されます。しかし、重い処理を伴う再レンダーはユーザー操作をブロックしてしまいます。
function TabContainer() {
const [tab, setTab] = useState('about');
// ❌ 重いコンポーネントへの切り替えでUIがフリーズ
return (
<>
<button onClick={() => setTab('posts')}>Posts</button>
{tab === 'posts' && <SlowPostsTab />} {/* 1000件のリストを描画 */}
</>
);
}
問題: 「Posts」ボタンをクリックすると、SlowPostsTab の描画が完了するまで UI がフリーズし、他のボタンをクリックできなくなります。
1.2 useTransition が解決すること
useTransition を使うと、緊急ではない更新をバックグラウンドで処理し、ユーザー操作への即時レスポンスを維持できます。
function TabContainer() {
const [tab, setTab] = useState('about');
const [isPending, startTransition] = useTransition();
function selectTab(nextTab) {
// ✅ トランジションでラップして、UIをブロックしない
startTransition(() => {
setTab(nextTab);
});
}
return (
<>
<button onClick={() => selectTab('posts')}>
{isPending ? 'Loading...' : 'Posts'}
</button>
{tab === 'posts' && <SlowPostsTab />}
</>
);
}
結果:
- ボタンクリック後も他の操作が可能
-
isPendingでローディング状態を表示 - 途中で別のタブをクリックすれば、前の遷移を中断
1.3 useTransition の API
const [isPending, startTransition] = useTransition()
| 返り値 | 説明 |
|---|---|
isPending |
トランジションが保留中(進行中)かどうかを示すブール値 |
startTransition |
更新をトランジションとしてマークする関数 |
startTransition の引数:
| 引数 | 説明 |
|---|---|
action |
1つ以上の setState を呼び出す関数(「アクション」と呼ばれる) |
startTransition(() => {
// この中のすべての setState がトランジションとしてマークされる
setTab(nextTab);
setData(newData);
});
💡 アクション (Action) とは
startTransition に渡される関数は「アクション」と呼ばれます。慣習として、コールバック props は action という名前にするか、末尾に Action サフィックスをつけるとよいでしょう。
1.4 似ているフックとの比較
useTransition vs useDeferredValue
| 項目 | useTransition |
useDeferredValue |
|---|---|---|
| 制御対象 | state 更新全体 | 特定の値のみ |
| 使い方 | 関数をラップ | 値をラップ |
| 保留状態 |
isPending で取得可 |
なし |
| 使用場面 | 更新を自分で制御できる場合 | props で受け取った値を遅延させたい場合 |
// useTransition: state 更新を制御
const [isPending, startTransition] = useTransition();
startTransition(() => {
setQuery(input);
});
// useDeferredValue: 値を遅延
const deferredQuery = useDeferredValue(query);
useTransition vs 通常の setState
| 項目 | useTransition |
通常の setState
|
|---|---|---|
| 優先度 | 低優先度(中断可能) | 高優先度 |
| ブロッキング | なし | あり |
| 中断 | 可能 | 不可 |
| Suspense 連携 | フォールバック抑制 | フォールバック表示 |
1.5 重要な注意点
| ルール | 説明 |
|---|---|
| ✅ ノンブロッキング | ユーザー操作を妨げない |
| ⚠️ 同期関数のみ |
startTransition に渡す関数は同期的であるべき |
| ⚠️ 制御可能な更新のみ |
setState にアクセスできる場合のみ使用可能 |
| ❌ 入力フィールドには使用不可 | テキスト入力の制御には使えない |
// ❌ 入力フィールドには使用できない
function SearchBox() {
const [text, setText] = useState('');
function handleChange(e) {
// トランジションで入力値を更新すると、タイピングが遅延する
startTransition(() => {
setText(e.target.value); // NG!
});
}
return <input value={text} onChange={handleChange} />;
}
// ✅ 入力と検索結果を分離する
function SearchBox() {
const [text, setText] = useState('');
const [results, setResults] = useState([]);
function handleChange(e) {
setText(e.target.value); // 入力は即座に更新
startTransition(() => {
setResults(search(e.target.value)); // 検索結果はトランジション
});
}
return <input value={text} onChange={handleChange} />;
}
2. useTransition の内部構造を徹底解剖
2.0 全体像: useTransition の処理フロー
🎣 useTransition(フック呼び出し)
↓
📝 初回: mountTransition / 更新: updateTransition
↓
📋 state フック(isPending用)を作成
↓
🔗 startTransition 関数を Fiber & Queue にバインド
↓
🏃 startTransition 実行時:
├── 1. 現在の優先度を保存
├── 2. ContinuousEventPriority に設定(低優先度化)
├── 3. ReactSharedInternals.T にトランジション情報をセット
├── 4. isPending を true に更新(optimistic update)
├── 5. アクション(コールバック)を実行
└── 6. 完了後に isPending を false に更新
2.1 エントリポイント: packages/react/src/ReactHooks.js
// packages/react/src/ReactHooks.js
export function useTransition(): [
boolean,
(callback: () => void, options?: StartTransitionOptions) => void,
] {
const dispatcher = resolveDispatcher();
return dispatcher.useTransition();
}
useTransition が呼ばれると、現在のディスパッチャの useTransition メソッドが呼び出されます。
2.2 コア実装: mountTransition と updateTransition
初回レンダー時の処理 (mountTransition)
// packages/react-reconciler/src/ReactFiberHooks.js
function mountTransition(): [
boolean,
(callback: () => void, options?: StartTransitionOptions) => void,
] {
const stateHook = mountStateImpl((false: Thenable<boolean> | boolean));
// The `start` method never changes.
const start = startTransition.bind(
null,
currentlyRenderingFiber,
stateHook.queue,
true, // pendingState
false, // finishedState
);
const hook = mountWorkInProgressHook();
hook.memoizedState = start;
return [false, start];
}
引用元: packages/react-reconciler/src/ReactFiberHooks.js#L3398-L3415
ポイント:
-
mountStateImpl(false)でisPending用の state フックを作成(初期値false) -
startTransition関数を現在の Fiber と Queue にバインド -
start関数は変わらない(安定した参照) -
[false, start]を返す
更新時の処理 (updateTransition)
// packages/react-reconciler/src/ReactFiberHooks.js
function updateTransition(): [
boolean,
(callback: () => void, options?: StartTransitionOptions) => void,
] {
const [booleanOrThenable] = updateState(false);
const hook = updateWorkInProgressHook();
const start = hook.memoizedState;
const isPending =
typeof booleanOrThenable === 'boolean'
? booleanOrThenable
: // This will suspend until the async action scope has finished.
useThenable(booleanOrThenable);
return [isPending, start];
}
引用元: packages/react-reconciler/src/ReactFiberHooks.js#L3417-L3430
ポイント:
-
updateStateで現在のisPending値を取得 - 値が
booleanならそのまま使用、ThenableならuseThenableでアンラップ -
start関数はmemoizedStateから取得(常に同じ参照)
2.3 startTransition の実装
// packages/react-reconciler/src/ReactFiberHooks.js
function startTransition<S>(
fiber: Fiber,
queue: UpdateQueue<S | Thenable<S>, BasicStateAction<S | Thenable<S>>>,
pendingState: S,
finishedState: S,
callback: () => mixed,
options?: StartTransitionOptions,
): void {
const previousPriority = getCurrentUpdatePriority();
// 低優先度(ContinuousEventPriority)に設定
setCurrentUpdatePriority(
higherEventPriority(previousPriority, ContinuousEventPriority),
);
const prevTransition = ReactSharedInternals.T;
const currentTransition: Transition = ({}: any);
// トランジション情報を設定
if (enableViewTransition) {
currentTransition.types = prevTransition !== null
? prevTransition.types
: null;
}
ReactSharedInternals.T = currentTransition;
// isPending を true に設定(楽観的更新)
dispatchOptimisticSetState(fiber, false, queue, pendingState);
try {
const returnValue = callback();
// 非同期アクションの場合の処理
if (
returnValue !== null &&
typeof returnValue === 'object' &&
typeof returnValue.then === 'function'
) {
const thenable = ((returnValue: any): Thenable<mixed>);
const thenableForFinishedState = chainThenableValue(
thenable,
finishedState,
);
dispatchSetStateInternal(
fiber,
queue,
(thenableForFinishedState: any),
requestUpdateLane(fiber),
);
} else {
// 同期アクションの場合
dispatchSetStateInternal(
fiber,
queue,
finishedState,
requestUpdateLane(fiber),
);
}
} catch (error) {
// エラーをラップして再スロー
const rejectedThenable: RejectedThenable<S> = {
then() {},
status: 'rejected',
reason: error,
};
dispatchSetStateInternal(
fiber,
queue,
rejectedThenable,
requestUpdateLane(fiber),
);
} finally {
setCurrentUpdatePriority(previousPriority);
ReactSharedInternals.T = prevTransition;
}
}
引用元: packages/react-reconciler/src/ReactFiberHooks.js#L3090-L3233
ポイント:
-
優先度の切り替え:
ContinuousEventPriority(低優先度)に設定することで、他の高優先度更新に割り込まれる -
楽観的更新:
dispatchOptimisticSetStateで即座にisPending = trueを設定 -
非同期サポート: コールバックが
Promiseを返す場合、完了までisPendingを維持 -
エラーハンドリング: エラーは
rejectedThenableとしてラップされ、useTransitionから再スローされる
2.4 レーンシステム: トランジションの優先度管理
React のレーン(Lane)システムは、更新の優先度を管理します。トランジションには専用のレーンが割り当てられています。
// packages/react-reconciler/src/ReactFiberLane.js
const TransitionLanes: Lanes = /* */ 0b0000000001111111111111100000000;
const TransitionLane1: Lane = /* */ 0b0000000000000000000000100000000;
const TransitionLane2: Lane = /* */ 0b0000000000000000000001000000000;
const TransitionLane3: Lane = /* */ 0b0000000000000000000010000000000;
// ... TransitionLane14 まで存在
export const SomeTransitionLane: Lane = TransitionLane1;
引用元: packages/react-reconciler/src/ReactFiberLane.js#L61-L79
レーンの優先度順序:
高優先度 ←────────────────────────────→ 低優先度
SyncLane → InputContinuousLane → DefaultLane → TransitionLanes → RetryLanes → IdleLane
(同期) (連続入力) (デフォルト) (トランジション) (リトライ) (アイドル)
| レーン | 用途 | 例 |
|---|---|---|
SyncLane |
同期的な更新 | ReactDOM.flushSync |
InputContinuousLane |
連続入力 | テキスト入力、スクロール |
DefaultLane |
通常の更新 | 一般的な setState
|
TransitionLanes |
トランジション | startTransition |
IdleLane |
アイドル時 | 低優先度のバックグラウンド処理 |
2.5 トランジションオブジェクトの構造
// packages/react/src/ReactStartTransition.js
export type Transition = {
types: null | TransitionTypes, // enableViewTransition
gesture: null | GestureProvider, // enableGestureTransition
name: null | string, // enableTransitionTracing only
startTime: number, // enableTransitionTracing only
_updatedFibers: Set<Fiber>, // DEV-only
...
};
ReactSharedInternals.T にトランジションオブジェクトが格納されます。これにより、React 内部で現在トランジション中かどうかを判定できます。
2.6 Suspense との連携
トランジションは Suspense と密接に連携し、ローディングフォールバックの表示を制御します。
// 通常の更新: フォールバックが表示される
setTab('posts'); // → <Suspense> のフォールバックが表示
// トランジション: フォールバックが抑制される
startTransition(() => {
setTab('posts'); // → 前のタブが表示されたまま(isPending = true)
});
内部的には、トランジションレーンでの更新は Suspense バウンダリの「表示抑制」フラグをセットします。
3. ユースケース
3.1 タブ切り替えの最適化
重いコンテンツを持つタブ間の切り替えを滑らかにする最も一般的なパターン:
function TabContainer() {
const [tab, setTab] = useState('about');
const [isPending, startTransition] = useTransition();
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
return (
<>
<TabButtons
activeTab={tab}
isPending={isPending}
onSelect={selectTab}
/>
<TabContent tab={tab} />
</>
);
}
3.2 フィルタリング・検索結果の遅延表示
検索入力で即時レスポンスを維持しつつ、重い結果表示を遅延:
function SearchableList({ items }) {
const [query, setQuery] = useState('');
const [filteredItems, setFilteredItems] = useState(items);
const [isPending, startTransition] = useTransition();
function handleSearch(e) {
const value = e.target.value;
setQuery(value); // 入力は即座に反映
startTransition(() => {
// 重いフィルタリングはトランジション
setFilteredItems(items.filter(item =>
item.name.toLowerCase().includes(value.toLowerCase())
));
});
}
return (
<div>
<input value={query} onChange={handleSearch} />
{isPending && <LoadingIndicator />}
<List items={filteredItems} />
</div>
);
}
3.3 ルーターナビゲーション
ページ遷移をトランジションでラップして、滑らかなナビゲーションを実現:
function Router() {
const [page, setPage] = useState('/');
const [isPending, startTransition] = useTransition();
function navigate(url) {
startTransition(() => {
setPage(url);
});
}
return (
<Suspense fallback={<Spinner />}>
<NavBar navigate={navigate} isPending={isPending} />
<PageContent page={page} />
</Suspense>
);
}
3.4 非同期アクションの実行
startTransition は非同期関数もサポート(React 19+):
function CheckoutForm() {
const [quantity, setQuantity] = useState(1);
const [isPending, startTransition] = useTransition();
async function updateQuantityAction(newQuantity) {
startTransition(async () => {
const savedQuantity = await updateQuantity(newQuantity);
// ⚠️ await 後の setState は別の startTransition が必要
startTransition(() => {
setQuantity(savedQuantity);
});
});
}
return (
<div>
<QuantitySelector
value={quantity}
onChange={updateQuantityAction}
disabled={isPending}
/>
{isPending && <span>Updating...</span>}
</div>
);
}
3.5 親コンポーネントへのアクション公開
コンポーネントが props として action を公開するパターン:
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={async () => {
startTransition(async () => {
// action は同期でも非同期でも OK
await action();
});
}}>
{children}
</button>
);
}
// 使用側
<TabButton action={() => setTab('posts')}>Posts</TabButton>
4. トラブルシューティング
4.1 トランジション中に入力フィールドを更新できない
// ❌ これは動作しない
function handleChange(e) {
startTransition(() => {
setText(e.target.value);
});
}
解決策: 入力用の state とトランジション用の state を分離する
function handleChange(e) {
const value = e.target.value;
setText(value); // 即座に更新
startTransition(() => {
setSearchResults(search(value)); // これはトランジション
});
}
4.2 await 後の setState がトランジションにならない
// ❌ await 後の setState はトランジション外
startTransition(async () => {
await fetchData();
setData(newData); // トランジションとしてマークされない
});
// ✅ await 後も startTransition でラップ
startTransition(async () => {
await fetchData();
startTransition(() => {
setData(newData);
});
});
4.3 setTimeout 内の setState がトランジションにならない
// ❌ setTimeout 内はトランジション外
startTransition(() => {
setTimeout(() => {
setPage('/about'); // トランジションにならない
}, 1000);
});
// ✅ setTimeout 内で startTransition を呼ぶ
setTimeout(() => {
startTransition(() => {
setPage('/about');
});
}, 1000);
5. まとめ
useTransition は React の Concurrent Features の中核をなすフックです。
| 特徴 | 説明 |
|---|---|
| ノンブロッキング更新 | ユーザー操作を妨げない |
| 中断可能なレンダリング | 新しい更新が来たら前の処理を中断 |
| Suspense 連携 | フォールバック表示を抑制 |
| 保留状態の追跡 |
isPending でローディング UI を表示 |
| 優先度制御 | 低優先度レーンで処理 |
使用すべき場面:
- タブ切り替え、ページナビゲーション
- 検索・フィルタリング結果の表示
- 非同期データの取得と表示
- 重い計算を伴う UI 更新
避けるべき場面:
-
制御されたテキスト入力
-
即時レスポンスが必要な操作
-
他のコンポーネントから受け取った props(→
useDeferredValueを使用)