Reputation: 3525
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
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
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:
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
Reputation: 21
For someone using <RouterProvider router={router} />
it can be integrated in a component with <Outlet/>
. More details were described here.
Upvotes: 0
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
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
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
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
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
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
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
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
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
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
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
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
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
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
Reputation: 31
Using useEffect() - Solution for Functional Component
useEffect(() => {
window.history.scrollRestoration = 'manual';}, []);
Upvotes: 0
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
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
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
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
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 screenDepending on if you're loading data (think spinners) or if you have page transition animations, useEffect
may work better for you.
Upvotes: 44
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
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
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
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
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
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
Reputation: 32767
but classes are so 2018
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