Search code examples
javascripthtmlreactjsreact-routerreact-router-dom

React Router 6.15 scroll restoration while using createRoutesFromElements


I've been trying to implement the "scrollToTop" feature/component in my React project but to no avail.

A. I don't know where to put the ScrollToTop component suggested to be used in the docs (https://v5.reactrouter.com/web/guides/scroll-restoration) in the router provider

import "./App.css";
import {
    Route,
    RouterProvider,
    createBrowserRouter,
    createRoutesFromElements,
} from "react-router-dom";
import Layout from "./components/Layout";
import Home from "./pages/Home";
import Day1 from "./pages/Day1";
import Day2 from "./pages/Day2";
import Day3 from "./pages/Day3";
import DayC from "./pages/DayC";

export default function App() {
    const router = createBrowserRouter(
        createRoutesFromElements(
            <Route path="/" element={<Layout />}>
{/* If I put it somewhere here it returns an error stating that all components children of routes must be <Route> or <Fragment> */}
                <Route index element={<Home />} />
                <Route path="day1" element={<Day1 />} />
                <Route path="day2" element={<Day2 />} />
                <Route path="day3" element={<Day3 />} />
                <Route path="dayC" element={<DayC />} />
            </Route>
        )
    );

    return (
        <>
            <RouterProvider router={router} />
        </>
    );
}

B. By itself, rendering the <ScrollToTop> component returns the error "useLocation can only be used in the context of a Route component"

I don't know how to use it correctly. My desired effect is just for the page to scroll upwards on navigation since the content on the landing page is pretty lengthy.


Solution

  • Any React component rendering or using react-router-dom components or hooks necessarily needs to be rendered/called within the sub-ReactTree and context provided by a router. In this case it is the RouterProvider component.

    You can't call/use ScrollToTop directly in createBrowserRouter or createRoutesFromElements as these functions expect a RouteObject[] or JSX of Routes respectively.

    Either add the logic to the root Layout route component, or create another layout route component specifically for this purpose.

    Examples:

    import { useEffect } from "react";
    import { useLocation } from "react-router-dom";
    
    export default function ScrollToTop() {
      const { pathname } = useLocation();
    
      useEffect(() => {
        window.scrollTo(0, 0);
      }, [pathname]);
    
      return null;
    }
    
    import { Outlet } from 'react-router-dom';
    import { ScrollToTop } from '../components';
    
    const Layout = () => {
      ...
    
      return (
        ...
        <ScrollToTop />
        ...
        <Outlet />
        ...
      );
    };
    

    or as a hook

    import { useEffect } from "react";
    import { useLocation } from "react-router-dom";
    
    export default function useScrollToTop() {
      const { pathname } = useLocation();
    
      useEffect(() => {
        window.scrollTo(0, 0);
      }, [pathname]);
    }
    
    import { Outlet } from 'react-router-dom';
    import { useScrollToTop } from '../hooks';
    
    const Layout = () => {
      ...
    
      useScrollToTop();
    
      ...
    
      return (
        ...
        <Outlet />
        ...
      );
    };
    

    As a layout route

    import { useEffect } from "react";
    import { Outlet, useLocation } from "react-router-dom";
    
    export default function ScrollToTopLayout() {
      const { pathname } = useLocation();
    
      useEffect(() => {
        window.scrollTo(0, 0);
      }, [pathname]);
    
      return <Outlet />;
    }
    
    const router = createBrowserRouter(
      createRoutesFromElements(
        <Route element={<ScrollToTopLayout />}>
          <Route path="/" element={<Layout />}>
            <Route index element={<Home />} />
            <Route path="day1" element={<Day1 />} />
            <Route path="day2" element={<Day2 />} />
            <Route path="day3" element={<Day3 />} />
            <Route path="dayC" element={<DayC />} />
          </Route>
        </Route>
      )
    );
    
    export default function App() {
      return <RouterProvider router={router} />;
    }
    

    There also already exists a RRDv6 ScrollRestoration component that should scroll back to the top of the screen upon new location keys (i.e. a new entry in the history stack), and restores the scroll position upon navigating back to a previous page. It only works in Data routers, e.g. the createBrowserRouter you are using, and only needs to be rendered once in the root of the app, e.g. in Layout.

    import { Outlet, ScrollRestoration } from 'react-router-dom';
    
    const Layout = () => {
      ...
    
      return (
        ...
        <ScrollRestoration />
        ...
        <Outlet />
        ...
      );
    };
    

    Review the ScrollRestoration docs for further customizations.