200
votes

I have an issue when navigating into another page, its position will remain like the page before. So it won't scroll to top automatically. I've also tried to use window.scrollTo(0, 0) on onChange router. I've also used scrollBehavior to fix this issue but it didn't work. Any suggestions about this?

23
Could you not do the logic in componentDidMount of the new route's component?Yuya
just add document.body.scrollTop = 0; in the componentDidMount of the component you are moving toJohn Ruddell
@Kujira i've already added scrollTo inside componentDidMount() but it didnt work.adrian hartanto
@JohnRuddell That was not working too.adrian hartanto
There are some good solutions here but what if a link is clicked with an anchor id?Nathan Hensher

23 Answers

198
votes

but classes are so 2018

ScrollToTop implementation with React Hooks

ScrollToTop.js

import { useEffect } from 'react';
import { withRouter } from 'react-router-dom';

function ScrollToTop({ history }) {
  useEffect(() => {
    const unlisten = history.listen(() => {
      window.scrollTo(0, 0);
    });
    return () => {
      unlisten();
    }
  }, []);

  return (null);
}

export default withRouter(ScrollToTop);

Usage:

<Router>
  <Fragment>
    <ScrollToTop />
    <Switch>
        <Route path="/" exact component={Home} />
    </Switch>
  </Fragment>
</Router>

ScrollToTop can also be implemented as a wrapper component:

ScrollToTop.js

import React, { useEffect, Fragment } from 'react';
import { withRouter } from 'react-router-dom';

function ScrollToTop({ history, children }) {
  useEffect(() => {
    const unlisten = history.listen(() => {
      window.scrollTo(0, 0);
    });
    return () => {
      unlisten();
    }
  }, []);

  return <Fragment>{children}</Fragment>;
}

export default withRouter(ScrollToTop);

Usage:

<Router>
  <ScrollToTop>
    <Switch>
        <Route path="/" exact component={Home} />
    </Switch>
  </ScrollToTop>
</Router>
156
votes

This answer is only for v4 and not later versions.

The documentation for React Router v4 contains code samples for scroll restoration. Here is their first code sample, which serves as a site-wide solution for “scroll to the top” when a page is navigated to:

class ScrollToTop extends Component {
  componentDidUpdate(prevProps) {
    if (this.props.location !== prevProps.location) {
      window.scrollTo(0, 0)
    }
  }

  render() {
    return this.props.children
  }
}

export default withRouter(ScrollToTop)

Then render it at the top of your app, but below Router:

const App = () => (
  <Router>
    <ScrollToTop>
      <App/>
    </ScrollToTop>
  </Router>
)

// or just render it bare anywhere you want, but just one :)
<ScrollToTop/>

^ copied directly from the documentation

Obviously this works for most cases, but there is more on how to deal with tabbed interfaces and why a generic solution hasn't been implemented.

42
votes

React 16.8+

If you are running React 16.8+ this is straightforward to handle with a component that will scroll the window up on every navigation:
Here is in scrollToTop.js component

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;
}

Then render it at the top of your app, but below Router
Here is in app.js

import ScrollToTop from "./scrollToTop";

function App() {
  return (
    <Router>
      <ScrollToTop />
      <App />
    </Router>
  );
}

or in index.js

import ScrollToTop from "./scrollToTop";

ReactDOM.render(
    <BrowserRouter>
        <ScrollToTop />
        <App />
    </BrowserRouter>
    document.getElementById("root")
);
33
votes

This answer is for legacy code, for router v4+ check other answers

<Router onUpdate={() => window.scrollTo(0, 0)} history={createBrowserHistory()}>
  ...
</Router>

If it's not working, you should find the reason. Also inside componentDidMount

document.body.scrollTop = 0;
// or
window.scrollTo(0,0);

you could use:

componentDidUpdate() {
  window.scrollTo(0,0);
}

you could add some flag like "scrolled = false" and then in update:

componentDidUpdate() {
  if(this.scrolled === false){
    window.scrollTo(0,0);
    scrolled = true;
  }
}
31
votes

A React Hook you can add to your Route component. Using useLayoutEffect instead of custom listeners.

import React, { useLayoutEffect } from 'react';
import { Switch, Route, useLocation } from 'react-router-dom';

export default function Routes() {
  const location = useLocation();
  // Scroll to top if path changes
  useLayoutEffect(() => {
    window.scrollTo(0, 0);
  }, [location.pathname]);

  return (
      <Switch>
        <Route exact path="/">

        </Route>
      </Switch>
  );
}

Update: Updated to use useLayoutEffect instead of useEffect, for less visual jank. Roughly this translates to:

  • useEffect: render components -> paint to screen -> scroll to top (run effect)
  • useLayoutEffect: render components -> scroll to top (run effect) -> paint to screen

Depending on if you're loading data (think spinners) or if you have page transition animations, useEffect may work better for you.

27
votes

For react-router v4, here is a create-react-app that achieves the scroll restoration: http://router-scroll-top.surge.sh/.

To achieve this you can create decorate the Route component and leverage lifecycle methods:

import React, { Component } from 'react';
import { Route, withRouter } from 'react-router-dom';

class ScrollToTopRoute extends Component {
  componentDidUpdate(prevProps) {
    if (this.props.path === this.props.location.pathname && this.props.location.pathname !== prevProps.location.pathname) {
      window.scrollTo(0, 0)
    }
  }

  render() {
    const { component: Component, ...rest } = this.props;

    return <Route {...rest} render={props => (<Component {...props} />)} />;
  }
}

export default withRouter(ScrollToTopRoute);

On the componentDidUpdate we can check when the location pathname changes and match it to the path prop and, if those satisfied, restore the window scroll.

What is cool about this approach, is that we can have routes that restore scroll and routes that don't restore scroll.

Here is an App.js example of how you can use the above:

import React, { Component } from 'react';
import { BrowserRouter as Router, Route, Link } from 'react-router-dom';
import Lorem from 'react-lorem-component';
import ScrollToTopRoute from './ScrollToTopRoute';
import './App.css';

const Home = () => (
  <div className="App-page">
    <h2>Home</h2>
    <Lorem count={12} seed={12} />
  </div>
);

const About = () => (
  <div className="App-page">
    <h2>About</h2>
    <Lorem count={30} seed={4} />
  </div>
);

const AnotherPage = () => (
  <div className="App-page">
    <h2>This is just Another Page</h2>
    <Lorem count={12} seed={45} />
  </div>
);

class App extends Component {
  render() {
    return (
      <Router>
        <div className="App">
          <div className="App-header">
            <ul className="App-nav">
              <li><Link to="/">Home</Link></li>
              <li><Link to="/about">About</Link></li>
              <li><Link to="/another-page">Another Page</Link></li>
            </ul>
          </div>
          <Route exact path="/" component={Home} />
          <ScrollToTopRoute path="/about" component={About} />
          <ScrollToTopRoute path="/another-page" component={AnotherPage} />
        </div>
      </Router>
    );
  }
}

export default App;

From the code above, what is interesting to point out is that only when navigating to /about or /another-page the scroll to top action will be preformed. However when going on / no scroll restore will happen.

The whole codebase can be found here: https://github.com/rizedr/react-router-scroll-top

17
votes

It is noteable that the onUpdate={() => window.scrollTo(0, 0)} method is outdated.

Here is a simple solution for react-router 4+.

const history = createBrowserHistory()

history.listen(_ => {
    window.scrollTo(0, 0)  
})

<Router history={history}>
10
votes

React hooks 2020 :)

import React, { useLayoutEffect } from 'react';
import { useLocation } from 'react-router-dom';

const ScrollToTop: React.FC = () => {
  const { pathname } = useLocation();
  useLayoutEffect(() => {
    window.scrollTo(0, 0);
  }, [pathname]);

  return null;
};

export default ScrollToTop;
8
votes

In a component below <Router>

Just add a React Hook (in case you are not using a React class)

  React.useEffect(() => {
    window.scrollTo(0, 0);
  }, [props.location]);
6
votes

I had the same issue with my application.Using the below code snippet helped me scroll to the top of the page on click of the next button.

<Router onUpdate={() => window.scrollTo(0, 0)} history= {browserHistory}>
...
</Router>

However, the issue still persisted on browser back. After a lot of trials, realized that this was because of the browser window's history object, which has a property scrollRestoration which was set to auto.Setting this to manual solved my problem.

function scrollToTop() {
    window.scrollTo(0, 0)
    if ('scrollRestoration' in history) {
        history.scrollRestoration = 'manual';
    }
}

<Router onUpdate= {scrollToTop} history={browserHistory}>
....
</Router>
6
votes

Hooks are composable, and since React Router v5.1 we have a useHistory() hook. So based off @zurfyx's answer I've created a re-usable hook for this functionality:

// useScrollTop.ts
import { useHistory } from 'react-router-dom';
import { useEffect } from 'react';

/*
 * Registers a history listener on mount which
 * scrolls to the top of the page on route change
 */
export const useScrollTop = () => {
    const history = useHistory();
    useEffect(() => {
        const unlisten = history.listen(() => {
            window.scrollTo(0, 0);
        });
        return unlisten;
    }, [history]);
};
5
votes

This was my approach based on what everyone else had done in previous posts. Wondering if this would be a good approach in 2020 using location as a dependency to prevent re-renders?

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

function ScrollToTop( { children } ) {
    let location = useLocation();

    useEffect( () => {
        window.scrollTo(0, 0);
    }, [ location ] );

    return children
}
4
votes

I want to share my solution for those who are using react-router-dom v5 since none of these v4 solutions did the work for me.

What solved my problem was installing react-router-scroll-top and put the wrapper in the <App /> like this:

const App = () => (
  <Router>
    <ScrollToTop>
      <App/>
    </ScrollToTop>
  </Router>
)

and that's it! it worked!

4
votes

My solution: a component that I'm using in my screens components (where I want a scroll to top).

import { useLayoutEffect } from 'react';

const ScrollToTop = () => {
    useLayoutEffect(() => {
        window.scrollTo(0, 0);
    }, []);

    return null;
};

export default ScrollToTop;

This preserves scroll position when going back. Using useEffect() was buggy for me, when going back the document would scroll to top and also had a blink effect when route was changed in an already scrolled document.

1
votes

I wrote a Higher-Order Component called withScrollToTop. This HOC takes in two flags:

  • onComponentWillMount - Whether to scroll to top upon navigation (componentWillMount)
  • onComponentDidUpdate - Whether to scroll to top upon update (componentDidUpdate). This flag is necessary in cases where the component is not unmounted but a navigation event occurs, for example, from /users/1 to /users/2.

// @flow
import type { Location } from 'react-router-dom';
import type { ComponentType } from 'react';

import React, { Component } from 'react';
import { withRouter } from 'react-router-dom';

type Props = {
  location: Location,
};

type Options = {
  onComponentWillMount?: boolean,
  onComponentDidUpdate?: boolean,
};

const defaultOptions: Options = {
  onComponentWillMount: true,
  onComponentDidUpdate: true,
};

function scrollToTop() {
  window.scrollTo(0, 0);
}

const withScrollToTop = (WrappedComponent: ComponentType, options: Options = defaultOptions) => {
  return class withScrollToTopComponent extends Component<Props> {
    props: Props;

    componentWillMount() {
      if (options.onComponentWillMount) {
        scrollToTop();
      }
    }

    componentDidUpdate(prevProps: Props) {
      if (options.onComponentDidUpdate &&
        this.props.location.pathname !== prevProps.location.pathname) {
        scrollToTop();
      }
    }

    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
};

export default (WrappedComponent: ComponentType, options?: Options) => {
  return withRouter(withScrollToTop(WrappedComponent, options));
};

To use it:

import withScrollToTop from './withScrollToTop';

function MyComponent() { ... }

export default withScrollToTop(MyComponent);
1
votes

Since, I use function components, here is how I managed to achieve it.

import { useEffect } from 'react';
import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom';

function ScrollToTop() {
    const { pathname } = useLocation();

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

    return null;
}

const IndexRoutes = () => {

    return (
        <BrowserRouter>
            <ScrollToTop />
            <Routes>
                <Route exact path="/">
                    <Home /> 
                </Route>
                /* list other routes below */
            </Routes>
        </BrowserRouter>
    );
};

export default IndexRoutes;

You can also refer the code from the below link

https://reactrouter.com/web/guides/scroll-restoration

0
votes

Here is another method.

For react-router v4 you can also bind a listener to change in history event, in the following manner:

let firstMount = true;
const App = (props) => {
    if (typeof window != 'undefined') { //incase you have server-side rendering too             
        firstMount && props.history.listen((location, action) => {
            setImmediate(() => window.scrollTo(0, 0)); // ive explained why i used setImmediate below
        });
        firstMount = false;
    }

    return (
        <div>
            <MyHeader/>            
            <Switch>                            
                <Route path='/' exact={true} component={IndexPage} />
                <Route path='/other' component={OtherPage} />
                // ...
             </Switch>                        
            <MyFooter/>
        </div>
    );
}

//mounting app:
render((<BrowserRouter><Route component={App} /></BrowserRouter>), document.getElementById('root'));

The scroll level will be set to 0 without setImmediate() too if the route is changed by clicking on a link but if user presses back button on browser then it will not work as browser reset the scroll level manually to the previous level when the back button is pressed, so by using setImmediate() we cause our function to be executed after browser is finished resetting the scroll level thus giving us the desired effect.

0
votes

with React router dom v4 you can use

create a scrollToTopComponent component like the one below

class ScrollToTop extends Component {
    componentDidUpdate(prevProps) {
      if (this.props.location !== prevProps.location) {
        window.scrollTo(0, 0)
      }
    }

    render() {
      return this.props.children
    }
}

export default withRouter(ScrollToTop)

or if you are using tabs use the something like the one below

class ScrollToTopOnMount extends Component {
    componentDidMount() {
      window.scrollTo(0, 0)
    }

    render() {
      return null
    }
}

class LongContent extends Component {
    render() {
      <div>
         <ScrollToTopOnMount/>
         <h1>Here is my long content page</h1>
      </div>
    }
}

// somewhere else
<Route path="/long-content" component={LongContent}/>

hope this helps for more on scroll restoration vist there docs hare react router dom scroll restoration

0
votes

In your router.js, just add this function in the router object. This will do the job.

scrollBehavior() {
        document.getElementById('app').scrollIntoView();
    },

Like this,

**Routes.js**

import vue from 'blah!'
import Router from 'blah!'

let router = new Router({
    mode: 'history',
    base: process.env.BASE_URL,
    scrollBehavior() {
        document.getElementById('app').scrollIntoView();
    },
    routes: [
            { url: "Solar System" },
            { url: "Milky Way" },
            { url: "Galaxy" },
    ]
});
-1
votes

I found that ReactDOM.findDomNode(this).scrollIntoView() is working. I placed it inside componentDidMount().

-1
votes

For smaller apps, with 1-4 routes, you could try to hack it with redirect to the top DOM element with #id instead just a route. Then there is no need to wrap Routes in ScrollToTop or using lifecycle methods.

-3
votes
render() {
    window.scrollTo(0, 0)
    ...
}

Can be a simple solution in case the props are not changed and componentDidUpdate() not firing.

-13
votes

This is hacky (but works): I just add

window.scrollTo(0,0);

to render();