How do people handle scroll restoration with react-router v4?

my solution:save window.scrollY for every pathname with a Map (ES6)

scroll-manager.tsx

import { FC, useEffect } from 'react';
import { useLocation } from 'react-router-dom';

export function debounce(fn: (...params: any) => void, wait: number): (...params: any) => void {
  let timer: any = null;
  return function(...params: any){
    clearTimeout(timer);
    timer = setTimeout(()=>{
      fn(...params)
    }, wait);
  }
}

export const pathMap = new Map<string, number>();

const Index: FC = () => {
  const { pathname } = useLocation();

  useEffect(() => {
    if (pathMap.has(pathname)) {
      window.scrollTo(0, pathMap.get(pathname)!)
    } else {
      pathMap.set(pathname, 0);
      window.scrollTo(0, 0);
    }
  }, [pathname]);

  useEffect(() => {
    const fn = debounce(() => {
      pathMap.set(pathname, window.scrollY);
    }, 200);

    window.addEventListener('scroll', fn);
    return () => window.removeEventListener('scroll', fn);
  }, [pathname]);

  return null;
};

export default Index;

App.tsx

function App() {

  return (
      <Router>
        <ScrollManager/>
        <Switch>...</Switch>
      </Router>
  );
}

You can also use pathMap.size === 1 to determine if the user entered the app for the first time


How do you handle your scroll restoration?

Turns out browsers have implementations of the history.scrollRestoration.

Maybe you can use that? Check these links out.

https://developer.mozilla.org/en/docs/Web/API/History#Specifications https://developers.google.com/web/updates/2015/09/history-api-scroll-restoration

In addition, I found an npm module that might be able to handle scroll restoration in react with ease, but this library only works with react router v3 and below

https://www.npmjs.com/package/react-router-restore-scroll https://github.com/ryanflorence/react-router-restore-scroll

I hope this can help.


I wound up using localStorage to track the scroll position - not sure this would handle all situations.

In this example, there's a Company page with a set of Stores, and each Store has a set of Display cases. I needed to track the scroll position of the display cases, so saved that to a 'storeScrollTop' key. There were 6 lines of code to add.

company.jsx:

// click on a store link
const handleClickStore = (evt) => {
  window.localStorage.removeItem('storeScrollTop') // <-- reset scroll value
  const storeId = evt.currentTarget.id
  history.push(`/store/${storeId}`)
}

store.jsx:

// initialize store page
React.useEffect(() => {
  // fetch displays
  getStoreDisplays(storeId).then(objs => setObjs(objs)).then(() => {
    // get the 'store' localstorage scrollvalue and scroll to it
    const scrollTop = Number(window.localStorage.getItem('storeScrollTop') || '0')
    const el = document.getElementsByClassName('contents')[0]
    el.scrollTop = scrollTop
  })
}, [storeId])

// click on a display link    
const handleClickDisplay = (evt) => {
  // save the scroll pos for return visit
  const el = document.getElementsByClassName('contents')[0]
  window.localStorage.setItem('storeScrollTop', String(el.scrollTop))
  // goto the display
  const displayId = evt.currentTarget.id
  history.push(`/display/${displayId}`)
}

The trickiest part was figuring out which element had the correct scrollTop value - I had to inspect things in the console until I found it.


Note that history.scrollRestoration is just a way of disabling the browser's automatic attempts at scroll restoration, which mostly don't work for single-page apps, so that they don't interfere with whatever the app wants to do. In addition to switching to manual scroll restoration, you need some sort of library that provides integration between the browser's history API, React's rendering, and the scroll position of the window and any scrollable block elements.

After not being able to find such a scroll restoration library for React Router 4, I created one called react-scroll-manager. It supports scrolling to top on navigation to a new location (aka history push) and scroll restoration on back/forward (aka history pop). In addition to scrolling the window, it can scroll any nested element that you wrap in an ElementScroller component. It also supports delayed/asynchronous rendering by using a MutationObserver to watch the window/element content up to a user-specified time limit. This delayed rendering support applies to scroll restoration as well as scrolling to a specific element using a hash link.

npm install react-scroll-manager

import React from 'react';
import { Router } from 'react-router-dom';
import { ScrollManager, WindowScroller, ElementScroller } from 'react-scroll-manager';
import { createBrowserHistory as createHistory } from 'history';

class App extends React.Component {
  constructor() {
    super();
    this.history = createHistory();
  }
  render() {
    return (
      <ScrollManager history={this.history}>
        <Router history={this.history}>
          <WindowScroller>
            <ElementScroller scrollKey="nav">
              <div className="nav">
                ...
              </div>
            </ElementScroller>
            <div className="content">
              ...
            </div>
          </WindowScroller>
        </Router>
      </ScrollManager>
    );
  }
}

Note that an HTML5 browser (10+ for IE) and React 16 are required. HTML5 provides the history API, and the library uses the modern Context and Ref APIs from React 16.