React Transitions
unstable_useTransitions prop は実験的なものであり、マイナー/パッチリリースで破壊的変更が加えられる可能性があります。注意して使用し、関連する変更についてはリリースノートに非常に注意してください。
React 18 では、「トランジション(transitions)」の概念が導入されました。これにより、緊急性の高い UI 更新とそうでない UI 更新を区別できるようになります。React Transitions と「コンカレントレンダリング(concurrent rendering)」の詳細については、React の公式ドキュメントを参照してください。
React 19 は、Actions と Transitions で非同期関数を使用するサポートを導入することで、非同期/コンカレントな状況を強化します。非同期 Transitions のサポートにより、Transition 中に state の更新を表示してユーザーに即座のフィードバックを示すことを可能にする新しいReact.useOptimistic hookも導入されました。
Transitions in React Router
React に Transitions が導入されたことで、React Router がナビゲーションと router state を管理する方法のストーリーは少し複雑になります。これらは強力な API ですが、いくつかのニュアンスと複雑さも伴います。私たちは React Router が新しい React の機能とシームレスに連携することを目指していますが、場合によっては、新しい React のやり方と、React Router アプリで既に利用しているパターン(例:保留中の state、optimistic UI)との間にいくらかの緊張が生じる可能性があります。
スムーズな採用を確実にするため、Transitions に関連する変更はオプトインの unstable_useTransitions フラグの背後に導入されており、破壊的変更なしにアップグレードできるようになっています。
現在の動作
私たちは、future.v7_startTransition フラグを介して、React Router 6.13.0 で React Router を Suspense により親和的にするために、まず React.startTransition を活用しました。v7 では、それがデフォルトの動作となり、すべての router state の更新は現在 React.startTransition でラップされています。
このデフォルトの動作には、unstable_useTransitions が解決するために設計された2つの潜在的な問題があります。
startTransitionで更新をラップしたくない正当なユースケースがいくつかあります。- 1つの具体的な問題は、
React.useSyncExternalStoreの更新が Transitions になり得ないことです(^1、^2)。useSyncExternalStoreは同期更新を強制するため、本来フォールバックの表示を避けるはずの更新トランジションでフォールバックが表示されてしまう可能性があります。 - React Router には、state の更新に
React.flushSyncを使用するためのflushSyncオプションがナビゲーションにありますが、それが常に適切な解決策とは限りません。
- 1つの具体的な問題は、
- React 19 は、新しい
startTransition(() => Promise))API と、Transitions 中に更新を表示するための新しいuseOptimistichook を追加しました。- React Router にいくつかの更新がないと、
startTransition(() => navigate(path))は期待通りに動作しません。これは、内部でuseOptimisticを使用していないため、ナビゲーション中に router state の更新が表示されず、useNavigationのような hook が機能しなくなるためです。
- React Router にいくつかの更新がないと、
上記の両方の問題に対する解決策を提供するために、router component に新しい unstable_useTransitions prop を導入します。これにより、router state の更新に startTransition を使用しないようにオプトアウト(最初の問題を解決)するか、startTransition + useOptimistic のより強化された使用法にオプトイン(2番目の問題を解決)することができます。現在の動作は新しい React 19 API とは少し不完全であるため、React Router v8 ではオプトインの動作をデフォルトにする予定ですが、useSyncExternalStore のようなユースケースのためにオプトアウトフラグは維持する可能性が高いです。
unstable_useTransitions=false によるオプトアウト
useSyncExternalStore の使用(またはその他の理由)により、アプリケーションが「Transition に対応していない」場合、prop を介してオプトアウトできます。
// Framework Mode (entry.client.tsx)
<HydratedRouter unstable_useTransitions={false} />
// Data Mode
<RouterProvider unstable_useTransitions={false} />
// Declarative Mode
<BrowserRouter unstable_useTransitions={false} />これにより、router は内部の state 更新を startTransition でラップしなくなります。
unstable_useTransitions=true によるオプトイン
React.useOptimistic へのアクセスが必要なため、React 19 を使用している必要があります。
コンカレントモードと Transitions に依存するすべての新しい React 19 の機能とアプリケーションをうまく連携させたい場合は、新しい prop を介してオプトインできます。
// Framework Mode (entry.client.tsx)
<HydratedRouter unstable_useTransitions />
// Data Mode
<RouterProvider unstable_useTransitions />
// Declarative Mode
<BrowserRouter unstable_useTransitions />このフラグが有効な場合:
- すべての内部 state の更新は
React.startTransitionでラップされます(フラグなしの現在の動作)。 - すべての
<Link>/<Form>ナビゲーションはReact.startTransitionでラップされ、useNavigate/useSubmitが返す Promise を使用して、Transition がナビゲーションの期間中続くようにします。useNavigate/useSubmitは自動的にReact.startTransitionでラップしないため、これらを直接使用することで Transition が有効なナビゲーションからオプトアウトできます。
- Framework/Data モードでは、ナビゲーション中の router state 更新の一部が
useOptimisticを介して UI に表示されます。- _進行中の_ナビゲーションおよびすべての fetcher 情報に関連する state が表示されます。
useNavigation()のstate.navigationuseRevalidator()のstate.revalidationuseActionData()のstate.actionDatauseFetcher()およびuseFetchers()のstate.fetchers
- 現在の location に関連する state は表示されません。
useLocationのstate.locationuseMatches()のstate.matchesuseLoaderData()のstate.loaderDatauseRouteError()のstate.errors- など
- _進行中の_ナビゲーションおよびすべての fetcher 情報に関連する state が表示されます。
このフラグを有効にすることで、アプリケーションの他の進行中の Transition が有効な側面とうまく連携する、完全に Transition が有効なナビゲーションが可能になります。
非同期 Transition で自動的にラップされる API は <Link> と <Form> だけです。それ以外のすべてについては、操作を自分で startTransition でラップする必要があります。
// 自動的に Transition が有効
<Link to="/path" />
<Form method="post" action="/path" />
// 手動で Transition が有効
startTransition(() => navigate("/path"));
startTransition(() => submit(data, { method: 'post', action: "/path" }));
startTransition(() => fetcher.load("/path"));
startTransition(() => fetcher.submit(data, { method: "post", action: "/path" }));
// Transition が無効
navigate("/path");
submit(data, { method: 'post', action: "/path" });
fetcher.load("/path");
fetcher.submit(data, { method: "post", action: "/path" });重要: startTransition の内部では、navigate Promise を常に return または await する必要があります。そうすることで、Transition がナビゲーションの全期間を網羅します。Promise を return または await するのを忘れると、Transition が途中で終了し、期待通りに動作しません。
// ✅ Promise を return
startTransition(() => navigate("/path"));
startTransition(() => {
setOptimistic(something);
return navigate("/path"));
});
// ✅ Promise を await
startTransition(async () => {
setOptimistic(something);
await navigate("/path"));
});
// ❌ Promise を return しない
startTransition(() => {
setOptimistic(something);
navigate("/path"));
});
// ❌ Promise を await しない
startTransition(async () => {
setOptimistic(something);
navigate("/path"));
});popstate ナビゲーション
現在、optimistic state と popstate にバグがあります。バックナビゲーション中に現在の route を読み取る必要があり、それが同期的に完了できない場合(例:キャッシュされていないデータで Suspends する場合)、バックナビゲーションする前に optimistic state を設定するか、タイマーまたはマイクロタスクで optimistic update を遅延させることができます。