Two ways to reset scroll to top on every page transition in React Router
— React, React Router, Web Development — 4 min read
By default, in a React Router application, after a page transition, the scroll doesn't get reset meaning if the user had scrolled somewhere on the page before the page transition, then after the page transition, the page remains scrolled at the same position.
In this post, we're going to learn how to reset scroll to the top of the page when a React Router app transitions to a new page.
You can download or clone the tutorial codebase from this github repo. But its optional as the code snippets below will explain the most important aspects of the problem and its solution.
In case you do want to follow along with the tutorial, after downloading or cloning it, run npm i
in your terminal to install dependencies and then npm run dev
to start a localhost server.
This tutorial assumes basic working knowledge of React and React Router. The tutorial also uses CSS classes and components from the Bootstrap library.
The starter files will showcase the default React Router behaviour as seen in the below screencast. As you can see, the scroll does not get reset when the user clicks on the nav menu links in the footer.
Lets dive into the implementation for this default behaviour and then we'll fix the problem with the solution.
There are two pages in this tutorial, <SomePage />
and <AnotherPage />
. Both display ten <Card />
components but the former shows cards in blue whereas the latter shows those cards in green.
Lets have a look at the /src/main.jsx
file. The main purpose of this file is to define our routes.
const router = createBrowserRouter([ { path: "/", element: <Root />, children: [ { path: "/", element: <SomePage /> }, { path: "/anotherpage", element: <AnotherPage /> } ] }])
ReactDOM.createRoot(document.getElementById('root')).render( <React.StrictMode> <RouterProvider router={router} /> </React.StrictMode>,)
Now lets look at /src/Root.jsx
file. The main purpose of this file is to dump child route components into <Outlet />
and render a footer nav bar.
export default function Root() { return ( <> <Outlet /> { /* Footer */ } <nav className="navbar navbar-expand-lg bg-body-tertiary"> <div className="container"> <ul className="navbar-nav"> <li className="nav-item"> <Link to="/" className="nav-link">SomePage</Link> </li> <li className="nav-item"> <Link to="/anotherpage" className="nav-link">AnotherPage</Link> </li> </ul> </div> </nav> </> );}
Here is the code for /src/SomePage.jsx
.
export default function SomePage() { const items = [1,2,3,4,5,6,7,8,9,10] const cards = items.map( i => <Card cardNum={i} key={i} className="bg-primary-subtle" /> )
return ( <div className="container my-5"> <div className="row"> <div className="col"> { cards } </div> </div> </div> );}
Reset scroll using <ScrollRestoration />
React Router provides a simple in-built way to restore scroll to its initial position on every page transition using the <ScrollRestoration />
component.
Go ahead and add it in your /src/Root.jsx
file.
import { Link, Outlet, ScrollRestoration } from "react-router-dom";
export default function Root() { return ( <> <Outlet /> <nav className="navbar navbar-expand-lg bg-body-tertiary"> ... </nav>
<ScrollRestoration /> </> );}
This is the output after you make the above changes in your code:
Reset scroll using useLayoutEffect()
and scrollTo()
While the first approach is much simpler, there may be situations where you do not want the user to see the scroll animation and instead just want the page to scroll to top instantly. This is where this approach comes in handy.
In order to reset scroll, we need to use the hook useLayoutEffect()
. We'll also use the scrollTo()
method to scroll to the top of the page and pass the value "instant"
to the behaviour
option to make the scroll animation unnoticeable, much in the same way as it'd happen if this was an actual server-rendered page transition.
Please note that we are using
useLayoutEffect()
and notuseEffect()
because we want to reset the scroll position before the new page gets rendered.
Go ahead and add the highlighted code into your /src/Root.jsx
file.
import { useLayoutEffect } from "react";import { Link, Outlet, useLocation } from "react-router-dom";
export default function Root() { const location = useLocation();
// scroll to top of page after a page transition. useLayoutEffect(() => { document.documentElement.scrollTo({ top:0, left:0, behavior: "instant" }); }, [location.pathname]); return ( <> <Outlet /> <nav className="navbar navbar-expand-lg bg-body-tertiary"> ... </nav> </> );}
The output of these changes will look like this:
As you can see, clicking on the page links resets the scroll and displays the top of the page being transitioned to without any animations.
Hope this helps!🙏