View Transitions と React Router の統合
React Router 6.4 の組み込み unstable_useViewTransitionState フックでルート切替をアニメーション。古いバージョン向けの flushSync による手動連携も解説。
Integration
詳細な説明
React Router 6.4+ ビルトインサポート
React Router 6.4(2024年3月)以降、すべての <Link> が viewTransition プロパティを受け付けます:
import { Link } from 'react-router-dom';
<Link to="/products/42" viewTransition>
Product 42
</Link>
ユーザーがクリックすると、React Router が内部でナビゲーションを document.startViewTransition() でラップします。追加設定は不要。
アクティブなトランジションごとの条件付きスタイリング
トランジション中のルートを別スタイルにするには useViewTransitionState フックを使います:
import { useViewTransitionState } from 'react-router-dom';
function ProductCard({ id }) {
const isTransitioning = useViewTransitionState(`/products/${id}`);
return (
<div style={{ viewTransitionName: isTransitioning ? `product-${id}` : 'none' }}>
…
</div>
);
}
カードが現在のトランジションのソースである 場合のみ 動的にオプトインされ、グリッド全体に一意の名前を付ける爆発を回避します。
フックなしの手動統合(古い React Router)
import { flushSync } from 'react-dom';
import { useNavigate } from 'react-router-dom';
function MyLink({ to, children }) {
const navigate = useNavigate();
return (
<a onClick={(e) => {
e.preventDefault();
if (!document.startViewTransition) return navigate(to);
document.startViewTransition(() => {
flushSync(() => navigate(to));
});
}}>{children}</a>
);
}
flushSync は React にナビゲーションを 同期的に コミットさせ、ブラウザが2枚目のスナップショットを即座に取れるようにします。これがないと React がアップデートをバッチ処理し、スナップショットが早すぎるタイミングで取られます。
Suspense バウンダリ
ルートが <Suspense> を使う場合、トランジションは Suspense バウンダリの解決を待ってからスナップショットを取ります。Suspense 内に意味のあるスケルトンを描画し、コンテンツとスピナーではなく2つのリアルなビュー間でクロスフェードさせてください。
ユースケース
React Router 6.4+ でナビゲーションする本番 React アプリ。同じアイテムが異なるレイアウトで現れる商品詳細ページ、写真ギャラリーのディープリンク、「リスト→詳細→リスト」フロー全般で特に強力。