adrian hartanto
adrian hartanto

Reputation: 3525

react-router scroll to top on every transition

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?

Upvotes: 349

Views: 301816

Answers (30)

Saeed Rohani
Saeed Rohani

Reputation: 3014

React 16.8+ (Works with React Router version 5+)

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 (React 18+)

import ScrollToTop from "./scrollToTop";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
    <BrowserRouter>
        <ScrollToTop />
        <App />
    </BrowserRouter>
);

index.js (React 16 - 17)

import ScrollToTop from "./scrollToTop";

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

Upvotes: 264

Sjoerd Van Den Berg
Sjoerd Van Den Berg

Reputation: 763

NOTE: This is a typescript solution using the React Router Dom v6 version with React 18+

Why I chose to also answer:

I've read through all the answers and not one provided me with the behavior I would expect as an end-user and by using the v6 package as well. The thing about the v6 version is that they removed some usefull utility functions, making this way harder than before. Also: this is still very much an issue as it was all those years ago.

The behavior we want:

  1. User navigates to a new page: scroll to the top immediately.
  2. User navigates back: the previous page is shown with scroll precisely where they left it.
  3. User navigates forward, the next page is shown with scroll precisely where they left it.

Using these 3 rules we catch every new/non-new page and we solve it in the same way plain HTML does in the browser.

The copy-paste answer:

ScrollToTop.tsx

import { useEffect, useRef } from "react";
import { useLocation } from "react-router-dom";

/**
 * Provides an HTML way of scrolling to the correct page part
 * when pushing, popping and replacing routes.
 * Note: This component expects `react-router-dom v6`.
 **/
const ScrollToTop = () => {
  const { pathname } = useLocation();

  /** We store the known history as a [pathname: string, scrollValue: number] array: */
  const storedScroll = useRef<[pathname: string, scrollValue: number][]>([]);

  useEffect(() => {
    /**
     * If the history has more items,
     * it means we have pushed a new page
     * so we store scroll=0. */
    while (history.length > storedScroll.current.length) {
      storedScroll.current.push([pathname, 0]);
    }
    /**
     * If the history has less items,
     * it means we have replaced a page earlier in history
     * so we remove excessive items from the list. */
    while (history.length > storedScroll.current.length) {
      storedScroll.current.pop();
    }

    /** double-check if we have idx and it is a number. */
    if (history.state.idx && !isNaN(history.state.idx)) {
      const stored = storedScroll.current[history.state.idx];
      if (stored[0] !== pathname) {
        /**
         * If the pathname changed, it means we replaced a page with a new one
         * so we store the new pathname and scroll=0.
         **/
        storedScroll.current[history.state.idx] = [pathname, 0];
      }
      /** We want to do this instantly as this is how plain HTML behaves as well. */
      window.scrollTo({
        top: storedScroll.current[history.state.idx][1],
        behavior: "instant",
      });
    }

    /** We listen to the scroll event to update the stored scroll value of the current pathname. */
    const onScroll = () => {
      storedScroll.current[history.state.idx][1] = window.scrollY;
    };
    window.addEventListener("scroll", onScroll);
    return () => {
      window.removeEventListener("scroll", onScroll);
    };
  }, [pathname]);

  return null;
};

export default ScrollToTop;

How to use this in your setup: main.tsx (or whatever works for you)

import { StrictMode } from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
import ScrollToTop from "./ScrollToTop";

ReactDOM.createRoot(document.getElementById("app") as HTMLElement).render(
  <React.StrictMode>
    <BrowserRouter>
      <ScrollToTop />
      <App />
    </BrowserRouter>
  </React.StrictMode>
);

What it doesn't do:

If you choose to have anything overflow instead of the document.body the scroll will not be remembered, and window.scrollTo() will not work. You could however then rewrite this code to use that element instead.

Hope this helps!

Upvotes: 0

Dybol
Dybol

Reputation: 21

For someone using <RouterProvider router={router} /> it can be integrated in a component with <Outlet/>. More details were described here.

Upvotes: 0

Unmitigated
Unmitigated

Reputation: 89234

If you want to not only have the scroll position reset to the top when navigating to a new page, but also to maintain the old scroll position when going to a previous page, use the ScrollRestoration component (available since React Router 6.4). It requires using a data router, such as one created by calling createBrowserRouter (which is recommended for all React Router web projects).

This component will emulate the browser's scroll restoration on location changes after loaders have completed to ensure the scroll position is restored to the right spot, even across domains.

Simply render it once in your root component:

import { ScrollRestoration } from 'react-router-dom';
function App() {
    return <>
        <div>Some content...</div>
        <ScrollRestoration/>
    </>;
}

Upvotes: 6

Nikita
Nikita

Reputation: 765

You could also just add

window.scrollTo(0, 0)

in the Link tag.

It would look, something like this:

            <Link
              to={yourUrl}
              onClick= {() => window.scrollTo(0, 0)}
            >
              <div className="lg:text-xl">Your Text</div>
            </Link>

Upvotes: 1

Dinith Rukshan Kumara
Dinith Rukshan Kumara

Reputation: 680

2022 November Update

Nothing work in react latest version 18.2.0 and react-router-dom 6.4.3. So I implemented this. Worked for me.Hope this helpful for anyone.

ScrollToTop.js

import { useEffect } from "react";
import { useLocation } from "react-router-dom";

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

  useEffect(() => {
    const body = document.querySelector('#root');
    body.scrollIntoView({
        behavior: 'smooth'
    }, 500)

}, [pathname]);

  return null;
}

Then import and add to browser router in index.js or App.js where your routes defined.

import { BrowserRouter, Routes, Route } from "react-router-dom";
import ScrollToTop from "./ScrollToTop";

function App() {
  return (
    <div>
      <BrowserRouter>
      <ScrollToTop />
        <Routes>
         //your routes
        </Routes>
      </BrowserRouter>
 </div>
  );
}

export default App;

(Note: Make sure the index.html div id="root".)

Upvotes: 5

Sergey P. aka azure
Sergey P. aka azure

Reputation: 4742

For my app, it's essential to have ability to scroll up when navigating by links. But it's also essential to not scroll anything when clicking on tab labels (which are also links). Also, my app has an advanced layout so scrollable containers could be different depending on the layout of the current page and where the link follows.

So here's my solution that works for me:

Introduce RouterLink wrapper around react-router-dom's Link that adds onClick handler. When link is clicked, it finds the nearest scrolled container and scrolls it up. It's possible to opt-out of this behavior by specifying preserveScroll (that's the default behavior of the original Link.

Hope that helps.

// Helper to find nearest scrolled parent of a given node
const getScrollParent = (node) => {
  if (!node) {
    return null;
  }

  if (node.scrollTop > 0) {
    return node;
  } else {
    return getScrollParent(node.parentNode);
  }
};

interface RouterLinkProps extends LinkProps {
  preserveScroll?: boolean;
}
export const RouterLink: React.FC<RouterLinkProps> = ({ preserveScroll = false, ...linkProps }) => {
  const handleClick = useCallback(
    (val) => {
      const targetEl = val?.target;
      if (!targetEl || preserveScroll) {
        return;
      }
      const scrolledContainer = getScrollParent(targetEl);
      if (scrolledContainer) {
        scrolledContainer.scrollTo({
          top: 0,
          left: 0,
          behavior: 'smooth',
        });
      }
    },
    [preserveScroll],
  );

  const extraProps = useMemo(() => {
    if (!preserveScroll) {
      return {
        onClick: handleClick,
      };
    }
    return {};
  }, [handleClick, preserveScroll]);

  return <Link {...linkProps} {...extraProps} />;
};

Now, I can use this wrapper and get desired behavior and enough control to adjust it. Like this:

<RouterLink to="/some/path">My Link that scrolls up</RouterLink>

Upvotes: 0

Thomas Aumaitre
Thomas Aumaitre

Reputation: 807

In your main component.

Just add this React Hooks (in case you are not using a React class) :

const oldPage = useRef(pathname)

useEffect(() => {
  if (pathname !== oldPage.current) {
    try {
      window.scroll({
        top: 0,
        left: 0,
        behavior: 'smooth'
      })
    } catch (error) {
      // for older browser
      window.scrollTo(0, 0)
    }
    oldPage.current = pathname
  }
}, [pathname])

Upvotes: 10

Hassan Shahzad
Hassan Shahzad

Reputation: 584

FOR 'REACT-ROUTER-DOM v6 & above'

I solved the following issue by creating a wrapper function and wrapping it around all the routes.

Follow the following steps:

1: You need to import the following:

import {Routes, Route, BrowserRouter as Router, useLocation} from 'react-router-dom';
import {useLayoutEffect} from 'react';

2: Write a wrapper function just above the "App" function:

const Wrapper = ({children}) => {
  const location = useLocation();
  useLayoutEffect(() => {
    document.documentElement.scrollTo(0, 0);
  }, [location.pathname]);
  return children
} 

3: Now wrap your routes within the wrapper function:

<BrowserRouter>
  <Wrapper>
    <Navbar />
      <Routes>
        <Route exact path="/" element={<Home/>} />
        <Route path="/Products" element={<Products/>} />
        <Route path="/Login" element={<Login/>} />
        <Route path="/Aggressive" element={<Aggressive/>} />
        <Route path="/Attendance" element={<Attendance/>} />
        <Route path="/Choking" element={<Choking/>} />
        <Route path="/EmptyCounter" element={<EmptyCounter/>} />
        <Route path="/FaceMask" element={<FaceMask/>} />
        <Route path="/Fainting" element={<Fainting/>} />
        <Route path="/Smoking" element={<Smoking/>} />
        <Route path="/SocialDistancing" element={<SocialDistancing/>} />
        <Route path="/Weapon" element={<Weapon/>} />
      </Routes>
    <Footer />
  </Wrapper>
</BrowserRouter>

This should solve the issue.

Upvotes: 18

atazmin
atazmin

Reputation: 5687

With smooth scroll option

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

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

  useEffect(() => {
    window.scrollTo({
      top: 0,
      left: 0,
      behavior: 'smooth', 
    });
  }, [pathname]);

  return null;
}

...

<Router>
      <ScrollToTop />
     ...
    </Router>

Upvotes: 7

Erik P_yan
Erik P_yan

Reputation: 798

I used Typescript in my project. This worked for me:

  // ScrollToTop.tsx
import {useEffect, useRef} from 'react'
import {withRouter} from 'react-router-dom'

const ScrollToTopComponent = () => {
  const mounted = useRef(false)

  useEffect(() => {
    if (!mounted.current) {
      //componentDidMount
      mounted.current = true
    } else {
      //componentDidUpdate
      window.scrollTo(0, 0)
    }
  })

  return null
}

export const ScrollToTop = withRouter(ScrollToTopComponent)



// usage in App.tsx
export default function App() {
  return (
     <Router>
        <ScrollToTop />
        <OtherRoutes />
     </Router>
  )
}

Upvotes: 0

Ricky Boyce
Ricky Boyce

Reputation: 1806

2021 (React 16) - Based off the comments from @Atombit

Below scrolls to top, but also preserves historic scroll positions.

function ScrollToTop() {
  const history = useHistory()
  useEffect(() => {
    const unlisten = history.listen((location, action) => {
      if (action !== 'POP') {
        window.scrollTo(0, 0)
      }
    })
    return () => unlisten()
  }, [])
  return (null)
}

Usage:

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

Upvotes: 7

Surafel Getachew
Surafel Getachew

Reputation: 44

for react-router-dom V5 react-router-dom V5 scroll to top

    import React, { useEffect } from 'react';
import {
    BrowserRouter as Router,
    Switch,
    Route,
    Link,
    useLocation,
    withRouter
} from 'react-router-dom'
function _ScrollToTop(props) {
    const { pathname } = useLocation();
    useEffect(() => {
        window.scrollTo(0, 0);
    }, [pathname]);
    return props.children
}
const ScrollToTop = withRouter(_ScrollToTop)
function App() {
    return (
        <div>
            <Router>
                <ScrollToTop>
                    <Header />
                     <Content />
                    <Footer />
                </ScrollToTop>
            </Router>
        </div>
    )
}

Upvotes: -1

Kassio
Kassio

Reputation: 29

My only solution was to add a line of code to each file like for example:

import React from 'react';
const file = () => { document.body.scrollTop = 0; return( <div></div> ) }

Upvotes: 0

AT1990
AT1990

Reputation: 41

Utilizing hooks, you can simply insert window.scrollTo(0,0) in your useEffect in your code base. Simply implement the code snippet in your app and it should load each page at the top of it's window.

import { useEffect } from 'react';

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

Upvotes: 4

Azhar
Azhar

Reputation: 119

August-2021

Rather then doing it in every page you can do this in App.js

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

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

Setting location in useEffect will make sure to scroll to top on every path change.

Upvotes: 10

Izabela Melo
Izabela Melo

Reputation: 21

For me, window.scrollTo(0, 0) and document.documentElement.scrollTo(0, 0) didn't work on all my screens (only worked on 1 screen).

Then, I realized that the overflow (where scrolling is allowed) of my screens were not in window (because we have some static points, so we putted the overflow: auto in other div).

I did the following test to realize this:

useEffect(() => {
  const unlisten = history.listen(() => {
    console.log(document.documentElement.scrollTop)
    console.log(window.scrollTop)
    window.scrollTo(0, 0);
  });
  return () => {
    unlisten();
  }
}, []);

In all the logs, I got 0.

So, I looked for which container I had the scroll in and put an id:

<div id="SOME-ID">
    ...
</div>

And in my ScrollToTop component I put:

useEffect(() => {
  const unlisten = history.listen(() => {
    window.scrollTo(0, 0);
    document.getElementById("SOME-ID")?.scrollTo(0, 0)
  });
  return () => {
    unlisten();
  }
}, []);

Now, when I go to a new route with history.push("/SOME-ROUTE") my screen go to the top

Upvotes: 2

Khushi Shivde
Khushi Shivde

Reputation: 31

Using useEffect() - Solution for Functional Component

 useEffect(() => {
window.history.scrollRestoration = 'manual';}, []);

Upvotes: 0

mtl
mtl

Reputation: 7557

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.

Upvotes: 166

Nishant Kohli
Nishant Kohli

Reputation: 485

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

Upvotes: 2

FreddieMixell
FreddieMixell

Reputation: 310

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
}

Upvotes: 7

Kepro
Kepro

Reputation: 402

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;

Upvotes: 15

Gabe M
Gabe M

Reputation: 2741

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.

Upvotes: 44

Sergiu
Sergiu

Reputation: 345

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.

Upvotes: 6

Debu Shinobi
Debu Shinobi

Reputation: 2572

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" },
    ]
});

Upvotes: 1

Kris Dover
Kris Dover

Reputation: 654

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

Upvotes: 7

Luis Febro
Luis Febro

Reputation: 1850

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!

Upvotes: 7

Eva Lond
Eva Lond

Reputation: 185

render() {
    window.scrollTo(0, 0)
    ...
}

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

Upvotes: -6

yakato
yakato

Reputation: 1

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.

Upvotes: -1

zurfyx
zurfyx

Reputation: 32767

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>

Upvotes: 284

Related Questions