Search code examples
javascriptreactjsreact-router-dom

How to prevent scroll-to-top on certain links in React-Router with a custom ScrollToTop component


I'm using React-Router with a custom ScrollToTop component to reset the scroll position to the top of the page on navigation. However, I have a specific use case where I need the scroll position to stay the same when clicking on category item.

ScrollToTop:

export const ScrollToTop = () => {
  const { pathname } = useLocation();

  useEffect(() => window.scrollTo(0, 0), [pathname]);

  return null;
};

Router:

<BrowserRouter>
  <ScrollToTop />
  <Routes>
    <Route path="/" element={<Layout />}>
      <Route path="/" element={<HomePage />}>
        <Route path=":category" element={<ExclusiveOffers />} />
      </Route>
      <Route path=":category/all" element={<AllProductsPage />} />
      <Route path=":category/:name" element={<ProductPage />} />
      <Route path="/account" element={<AccountPage />} />
      <Route path="/checkout" element={<CheckoutPage />} />
    </Route>
    <Route path="/auth" element={<AuthPage />} />
    <Route path="/login" element={<LoginPage />} />
    <Route path="/register" element={<RegisterPage />} />
  </Routes>
</BrowserRouter>

Category item:

<Box
  component={Link}
  to={title === "Top Categories"
    ? `/${item.query}`
    : `/${item.query}/all`
  }

Solution

  • You can accomplish skipping resetting the scroll-to-top a few different ways:

    1. Convert ScrollToTop to a layout route to wrap only the routes you want to scroll-to-top:

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

      Example Usage:

      <BrowserRouter>
        <Routes>
          <Route element={<Layout />}>
            <Route element={<ScrollToTopLayout />}>
              <Route index element={<HomePage />} />
              <Route path=":category/:name" element={<ProductPage />} />
              <Route path="account" element={<AccountPage />} />
              <Route path="checkout" element={<CheckoutPage />} />
            </Route>
      
            <Route path=":category" element={<ExclusiveOffers />} />
            <Route path=":category/all" element={<AllProductsPage />} />
          </Route>
      
          <Route element={<ScrollToTopLayout />}>
            <Route path="auth" element={<AuthPage />} />
            <Route path="login" element={<LoginPage />} />
            <Route path="register" element={<RegisterPage />} />
          </Route>
        </Routes>
      </BrowserRouter>
      
    2. Update ScrollToTop to consume explicit route paths to skip via props:

      import { useLocation, matchPath } from 'react-router-dom';
      
      export const ScrollToTop = ({ excludePaths = []}) => {
        const { pathname } = useLocation();
      
        useEffect(() => {
          const isExcluded = excludePaths.some(
            pattern => matchPath(pattern, pathname)
          );
      
          if (!isExcluded) {
            window.scrollTo(0, 0);
          }
        }, [excludePaths, pathname]);
      
        return null;
      };
      

      Example Usage:

      <BrowserRouter>
        <ScrollToTop excludePaths={[":category/all", ":category"]} />
      </BrowserRouter>
      

      Note however that ":category" would match other non-"category" route paths, like "/account" and "/login", so may not be ideal for all scenarios.

    3. Pass some link state to skip scrolling to top upon navigating:

      export const ScrollToTop = () => {
        const { pathname, state } = useLocation();
      
        useEffect(() => {
          if (!state?.preventScrollToTop) {
            window.scrollTo(0, 0);
          }
        }, [pathname, state]);
      
        return null;
      };
      

      Example Usage:

      <Box
        component={Link}
        to={title === "Top Categories"
          ? `/${item.query}`
          : `/${item.query}/all`
        }
        state={{ preventScrollToTop: true }}
      >
        ....
      </Box>
      

    You can use, mix, and apply any of the above implementations to cover more scenarios.