Search code examples
reactjstypescriptreact-router-domframer-motion

Framer Motion Page Transition with React Router DOM v6 causes rerender of layout components (esp. Sidebar) on Page Change


I recently added page transition to my React app using the library Framer Motion and React-Router-DOM. However, all layout components such as sidebar, navbar, etc. have been experiencing unwanted rerenders upon page change. Below is the snippet for my router and layout:

Router:

<AnimatePresence mode="wait">
   <Routes location={location} key={location.pathname}>
      <Route element={<GuestMiddleware />}>
         // Routes without layout
      </Route>

      <Route element={<AuthMiddleware />}>
         <Route element={<AdminLayout />}>   // <-- Here is the Layout
            // Routes with layout         
         </Route>
      </Route>
   </Routes>
</AnimatePresence>

Layout:

<>
   <Sidebar />

   <div className={cn("transition-spacing flex min-h-d-screen w-full flex-col ps-0 duration-300", { "lg:ps-sidebar": isOpen })}>
      <Topbar />

      <div className="grow">
         <Outlet />
      </div>
   </div>
</>

I actually found the reason for rerenders being the key attribute in the Routes component. But then once I remove it, all page exit transitions tend to behave incorrectly. I want to know if there is a way to fix or even a possible workaround regarding this.


Solution

  • It seems the issue is that the Sidebar component is rendered by the Layout component within the Routes using the current location.pathname as a React key. When the key changes, e.g. when the route location changes, the Routes component is remounted, including its entire sub-ReactTree.

    The solution is to move Sidebar out of the AnimatedPresence so it's not remounted when animating the route transitions.

    Layout.tsx

    Remove Sidebar from the Layout component.

    import { Outlet } from "react-router-dom";
    import Topbar from "../components/Topbar";
    import { cn } from "../utils/cn";
    import { useSidebarStore } from "../features/stores/sidebar";
    
    const Layout = () => {
      const { isOpen } = useSidebarStore();
    
      return (
        <main
          className={cn(
            "transition-[padding] duration-300 relative flex flex-col w-full min-h-screen",
            isOpen ? "ps-[300px]" : "ps-0",
          )}
        >
          <Topbar />
    
          <div className="grow">
            <Outlet />
          </div>
        </main>
      );
    };
    
    export default Layout;
    

    router.tsx

    Render Sidebar in the Router outside AnimatePresence.

    import { useLocation, Route, Routes } from "react-router-dom";
    import { AnimatePresence } from "framer-motion";
    
    import Layout from "./layouts/Layout";
    
    import Home from "./pages/Home";
    import About from "./pages/About";
    import Contact from "./pages/Contact";
    import Preferences from "./pages/settings/Preferences";
    import Users from "./pages/settings/Users";
    import Sidebar from "./components/Sidebar";
    
    const Router = () => {
      const location = useLocation();
    
      return (
        <>
          <Sidebar />
          <AnimatePresence mode="wait">
            <Routes {...{ location, key: location.pathname }}>
              <Route element={<Layout />}>
                <Route index element={<Home />} />
                <Route path="about" element={<About />} />
                <Route path="contact" element={<Contact />} />
                <Route path="settings">
                  <Route path="preferences" element={<Preferences />} />
                  <Route path="users" element={<Users />} />
                </Route>
              </Route>
            </Routes>
          </AnimatePresence>
        </>
      );
    };
    
    export default Router;