Scroll restoration with React Router

Scroll restoration with React Router

·

3 min read

Consider one example below.

We have 3 tabs on the top. Each tab has a list of items. We could scroll down to load more items for the tab. The tabs have style of sticky so even if we scroll down the list, we can alway click on other tabs and switch to other list.

Tab-A   Tab-B   Tab-C
--------+++++---------
list item 1
list item 2
list item 3
...

The issue we want to solve here is the scroll positions. Suppose we are one Tab-A and we scroll down to a position, say Position-A. Then we switch to Tab-B, scroll down, get Position-B. Now we have 3 things to consider:

  1. Switching to Tab-A by clicking on go back button

  2. Switching to Tab-A by clicking on the Tab-A button

  3. Switching to Tab-C by clicking on The Tab-C button

For the first case, since the user is clicking on the go back button explictly, we would expect go back to the Position-A. This is the default behavior in browsers. We don't need to worry about it unless we are breaking it by other things(like refresh the list by requesting new data).

For the second case, since user is clicking on the Tab-A button. The expected behavior may varies. We may want to go back to Position-A, or just go to top. The default behavior in browsers in this case is actually pretty strange, maybe its just me not getting the idea, which is still preserving the position of Position-B.

For the third case, since the user haven't access the tab, so the expected behavior should be just stay on top. But the default behavior in browsers now is also the same as second case, which is staying on Position-B.

So, let's see how to solve the issues above when we are using react-router v6 and its the data loader api.

First, import ScrollRestoration component. This is for react router taking over the ownership of controlling scroll position. This component should only import once, and it's recommended to render it in the root route.

import { ScrollRestoration } from "react-router-dom";

function RootRouteComponent() {
  return (
    <div>
      {/* ... */}
      <ScrollRestoration />
    </div>
  );
}

With this integration, if the request is to scroll to top for case 2 and 3, then the problem solved. This is the default behavior of react router, when navigation creates new scroll keys, the scroll position is reset to the top of the page. If you do not want this behavior, you can use this prop <Link preventScrollReset={true} />, the result is that you got the same behavior as in browsers.

If the request is to go back to Position-A for case 2, then we need additional setup.

With the ScrollRestoration component, React router will keep log of the scroll positions and attach with a special key string. The key is created automatically to identify each history entry. Attaching it with the key means each history entry will have its own scroll position. If we want to go back to Position-A for the new history entry, we can let this new history entry to use the same key string. To achieve that, we can define a custom getKey function.

<ScrollRestoration
  getKey={(location, matches) => {
    // return location.key; // default behavior
    return location.pathname;
  }}
/>

As you can see, we use return location.pathname; as key, since the new history entry has the same pathname as previous one, so they will share the same scroll position.

One thing needs to be noted that if you are using createHashRouter, we may tend to use locaiton.hash to be as the key. But the thing is that in the view of react router they are all pathname. So stick to pathname even if you are using hash as route.

That's all. Thanks for reading.