🎞️

React Router v6世代のページ遷移アニメーション

2023/08/13に公開

はじめに

React Routerがv6になり幾許の月日が流れましたが、
みなさまいかがMigrationでしょうか(会場大ウケ)。

本記事では以下ライブラリの利用を前提に、ページ遷移アニメーションの実装例を紹介します。

  • React Router v6
  • Framer Motion

各ライブラリの細かい実装方法の解説は割愛しています。ご了承ください。
最終的な実装はCodeSandboxを参照してください。
https://codesandbox.io/s/react-router-framer-motion-example-l8gnc9?file=/src/index.tsx

ページ遷移アニメーション

他記事見ると<Route>を使う方法が多く、RouterProviderを使った例があまりありませんでした。remixも混ざっていて、むずかしい。

また、Framer Motionを使っている記事も少ないです。
ここらへんが本記事のモチベーションになります。

RouterProviderを使う利点は割愛します。公式ドキュメントを参照してください。
https://reactrouter.com/en/main/upgrading/v6-data#migrating-to-routerprovider

以下のような実装をします。

index.tsx
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>,
);
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>
  );
};

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で定義したアニメーションが発火しません。

2. <Outlet />ではなくuseOutletを利用する

これも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をおすすめします。
https://www.framer.com/motion/animation/#variants

おわりに

はまりどころが多く苦労しましたが、あまり変なコトはせずに実装できました(当社比)。

Framer Motionは(ドキュメントが見にくい以外)使いやすいライブラリです。
今後も継続的に解説記事は書きたいです。よろしくお願いします。

Discussion