React Router v6世代のページ遷移アニメーション
はじめに
React Routerがv6になり幾許の月日が流れましたが、
みなさまいかがMigrationでしょうか(会場大ウケ)。
本記事では以下ライブラリの利用を前提に、ページ遷移アニメーションの実装例を紹介します。
- React Router v6
- Framer Motion
各ライブラリの細かい実装方法の解説は割愛しています。ご了承ください。
最終的な実装はCodeSandboxを参照してください。
ページ遷移アニメーション
他記事見ると<Route>
を使う方法が多く、RouterProviderを使った例があまりありませんでした。remixも混ざっていて、むずかしい。
また、Framer Motionを使っている記事も少ないです。
ここらへんが本記事のモチベーションになります。
RouterProviderを使う利点は割愛します。公式ドキュメントを参照してください。
以下のような実装をします。
const App = () => {
const matches = useMatches();
const outlet = useOutlet();
return (
<AnimatePresence>
<React.Fragment key={matches[1].pathname}>{outlet}</React.Fragment>
</AnimatePresence>
);
};
const router = [
{
path: '/',
element: <App />,
children: [
{ path: '/', element: <Home /> },
{
path: "/blog",
element: <Blog />,
children: [
{ path: "image-modal", element: <ImageModal />},
{ path: "detail-modal", element: <DetailModal />},
]
}
],
},
];
root.render(
<React.StrictMode>
<RouterProvider router={createBrowserRouter(router)} />
</React.StrictMode>,
);
const Home = () => {
return (
<motion.section
initial={{ y: "-100%" }}
animate={{ y: 1 }}
exit={{opacity: 0}}
transition={{duration: 0.3}},
>
<Link to="/blog">blog</Link>
</motion.section>
);
};
const Blog = () => {
return (
<motion.section
initial={{ y: "-100%" }}
animate={{ y: 1 }}
exit={{opacity: 1}}
transition={{duration: 0.3}},
>
<Link to="/">home</Link>
</motion.section>
);
};
App
Framer MotionのAnimatePresenceを使います。
要素が出たり消えたりするタイミングでアニメーションを再生してくれるすごいやつ。
const App = () => {
const matches = useMatches();
const outlet = useOutlet(); // useOutletはelementを返却する
return (
<AnimatePresence>
{/* Fragmentを使って一意のkeyを付与する */}
<React.Fragment key={matches[1].pathname}>{outlet}</React.Fragment>
</AnimatePresence>
);
};
ポイントは以下3つです。
1. Fragmentでkeyを付与する
AnimatePresenceの仕様上、childrenはkeyの付与が必須です。
やんねえとexitで定義したアニメーションが発火しません。
<Outlet />
ではなくuseOutlet
を利用する
2. これもkeyのための実装...ですが
理屈はよくわかりません。 動いたのでいいです。
rerenderとか、Framer Motion側の実装に起因すんのかなと考えています。
3. keyが変わるのはアニメーションさせたい遷移が起こったときのみ
後述しますが、すべてのページ遷移でアニメーションさせるならuseLocation().pathname
でも良いです。
router
const router = [
{
path: '/',
element: <App />,
children: [
{ path: '/', element: <Home /> },
{
path: "/blog",
element: <Blog />,
children: [
{ path: "image-modal", element: <ImageModal />},
{ path: "detail-modal", element: <DetailModal />},
]
}
],
},
];
rootに<App />
をelementとして定義します。
Outletは、ここでchildrenとして定義されたpathにマッチする要素が返却されます。
この例で言うとpathが/blog
なら、<App />
のuseOutlet
の返却値は<Blog />
になります。
AppでのkeyをuseMatches()[1].pathname
としたのは、
Blogのネストしたルーティングではアニメーションさせたくないため。
/blog
から/home
に遷移したときは遷移アニメーションされてほしい。
でも、/blog
から/blog/image-modal
に遷移するときは遷移アニメーションしたくない。
この実装のためにkeyを絞り込んでいます。難儀なものですね。
アニメーションしたいタイミングでkeyが変わらなければならない。
keyが変わらないとアニメーションしない点に注意してください。
Page
const Home = () => {
return (
<motion.section
initial={{ y: "-100%" }}
animate={{ y: 1 }}
exit={{opacity: 0}}
transition={{duration: 0.3}},
>
<Link to="/blog">blog</Link>
</motion.section>
);
};
アニメーションは各ページごとに実装を行えるようにしています。
別記事にする予定のモーダルアニメーションが理由です。
アニメーションの共通化はvariantsをおすすめします。
おわりに
はまりどころが多く苦労しましたが、あまり変なコトはせずに実装できました(当社比)。
Framer Motionは(ドキュメントが見にくい以外)使いやすいライブラリです。
今後も継続的に解説記事は書きたいです。よろしくお願いします。
Discussion