Why react new page render from the bottom of the screen? - javascript

I have multiple pages in my react project and, whenever I switch from one page to another the page position starts from the bottom but, I want to stay on top after rendering the page. I'm currently using router v5 and, I can't find any solution for router v5.
I've also tried another version solution but, It didn't work and could you please help me to solve this problem.
These are some of the examples I've tried so far but, not single ones have worked.
<Router onUpdate={() => window.scrollTo(0, 0)} history={createBrowserHistory()}>
...
</Router>
function handleUpdate() {
let {
action
} = this.state.location;
if (action === 'PUSH') {
window.scrollTo(0, 0);
}
}
...
<Router
onUpdate={handleUpdate}
/>

This is the solution I use to solve this problem. Create a new component:
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
const ScrollToTop = ({ children }) => {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return children || null;
};
export default ScrollToTop;
and wrap your app contents in it INSIDE of the browser router
import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import ScrollToTop from './components/scroll-to-top/scroll-to-top';
function App() {
return (
<div className="App">
<BrowserRouter>
<ScrollToTop>
<div />
</ScrollToTop>
</BrowserRouter>
</div>
);
}
export default App;

Create the next 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;
}
Import it in your index.js and use it before Switch component, like this:
<ScrollToTop />
<Switch>
...
</Switch>
And it should work.

The above methods of using window.scrollToView(0, 0) doesn't work for me. Thus, another workaround/solution which works for me is to have a div at the top with a ref. The scrollIntoView method would then be called in the useEffect hook to scroll the div into view on render.
import React, { useEffect, useRef } from "react";
const TestComponent = () => {
const topContainer = useRef();
useEffect(() => {
// To make sure page starts from the top
topContainer.current.scrollIntoView({ block: "end", behavior: 'smooth' });
}, []);
return (
<>
<div ref={topContainer} />
<div>Component to be rendered here</div>
</>
);
};
The property behavior: 'smooth' passed to scrollIntoView could be omitted too, if you do not want smooth animation of the scroll.

Related

Need scroll to be at top on new page load - React

I have a multipage application. I need to implement scroll to top automatically when I traverse to a new page. I have tried following:
ScrollToTop.js this is placed inside component folder
import React, { Component } from 'react';
import { withRouter } from 'react-router';
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)
And App.js
import {Router} from 'react-router-dom'
import ScrollToTop from './common/components/ScrollToTop'
const Appnew = () => (
<Router>
<ScrollToTop>
<Appnew/>
</ScrollToTop>
</Router>
)
const App = (props) => {
{Appnew()}
}
This is not working. Any suggestions?
There is nothing wrong in your hoc. Assuming your Appnew has components encapsulated with Route, it should work.
Just used your hoc and prepared a demo which is working fine. Take a look.
You should try to change this
componentDidUpdate(prevProps) {
if (this.props.location !== prevProps.location) {
window.scrollTo(0, 0)
}
}
by
componentDidMount() {
window.scroll({
top: 0,
left: 0
});
}
What it will do it's when the component is mounted it will scroll to the top.
Also you could use the event below which listen the DOM and when it is loaded do whatever you want to
window.addEventListener('DOMContentLoaded', (event) => {
// Your code
});

How to detect if another component is present in the document?

I have a site built with React Static that has a Header component that is always present. Depending on if the current page has a hero component or not, the Header should be either light or dark.
The Header is rendered outside of the routes and the useEffect is triggered before the children is rendered. This is probably because of the routing.
This is the current code:
// App.js
import React, { useState, useEffect } from 'react'
import { Root, Routes } from 'react-static'
export default () => {
const [useDarkTheme, setUseDarkTheme] = useState(false);
useEffect(() => {
if (typeof document !== "undefined") {
const heroPresent = document.querySelectorAll(".o-hero").length > 0;
console.log("The hero is present: " + heroPresent);
setUseDarkTheme(!heroPresent);
}
})
return (
<Root>
<React.Suspense fallback={ <em>Loading...</em> }>
<Header useDarkTheme={ useDarkTheme } />
<Routes default />
</React.Suspense>
</Root>
);
}
What will be rendered at <Routes default /> is the static pages configured in React Static's static.config.js.
Below is an example of the Hero component:
// Hero.js
import React from "react";
export default () => {
console.log("This is the Hero rendering. If this exist, the Header should be dark.");
return (
<div className="o-hero">
<p>Hero!</p>
</div>
);
}
When I run the application and look at the logs this is what I get:
The hero is present: false
This is the Hero rendering. If this exist, the Header should be dark.
How could I somehow detect the presence of the Hero from the Header although the Hero is in a router and the Header is not? This feels like quite a common use case, but I could not find any info on the interwebs.
Thanks in advance!
So I ended up using useContext to provide all children with a getter and a setter for the Header's theme (dark or light). The solution is very much inspired from this answer. The solution looks like this:
// App.js
import React, { useState, useContext } from 'react'
import { Root, Routes } from 'react-static'
import { HeaderThemeContext } from "./context";
export default () => {
const { theme } = useContext(HeaderThemeContext);
const [headerTheme, setHeaderTheme] = useState(theme);
return (
<Root>
<React.Suspense fallback={ <em>Loading...</em> }>
<HeaderThemeContext.Provider value={ { theme: headerTheme, setTheme: setHeaderTheme } }>
<Header theme={ headerTheme } />
<Routes default />
</HeaderThemeContext.Provider>
</React.Suspense>
</Root>
);
}
// Hero.js
import React from "react";
import { headerThemes, setHeaderTheme } from "./context";
export default () => {
setHeaderTheme(headerThemes.DARK);
console.log("This is the Hero rendering. If this exist, the Header should be dark.");
return (
<div className="o-hero">
<p>Hero!</p>
</div>
);
}
// context.js
import React, { createContext, useContext } from "react";
export const headerThemes = {
LIGHT: "light",
DARK: "dark",
};
export const HeaderThemeContext = createContext({
theme: headerThemes.LIGHT,
setTheme: () => {}
});
// This is a hook and can only be used in a functional component with access to the HeaderThemeContext.
export const setHeaderTheme = theme => useContext(HeaderThemeContext).setTheme(theme);
This gives global access to set and get the header theme, which might not be optional, but it works for now and I think it's fine. Please let me know if there is a better way of doing this.

Passing react-router-dom's Link into external library

I'm rendering components from my external (node_modules) pattern library. In my main App, I'm passing my Link instance from react-router-dom into my external libraries' component like so:
import { Link } from 'react-router-dom';
import { Heading } from 'my-external-library';
const articleWithLinkProps = {
url: `/article/${article.slug}`,
routerLink: Link,
};
<Heading withLinkProps={articleWithLinkProps} />
In my library, it's rendering the Link as so:
const RouterLink = withLinkProps.routerLink;
<RouterLink
to={withLinkProps.url}
>
{props.children}
</RouterLink>
The RouterLink seems to render correctly, and even navigates to the URL when clicked.
My issue is that the RouterLink seems to have detached from my App's react-router-dom instance. When I click Heading, it "hard" navigates, posting-back the page rather than routing there seamlessly as Link normally would.
I'm not sure what to try at this point to allow it to navigate seamlessly. Any help or advice would be appreciated, thank you in advance.
Edit: Showing how my Router is set up.
import React from 'react';
import { hydrate, unmountComponentAtNode } from 'react-dom';
import { AppContainer } from 'react-hot-loader';
import { Provider } from 'react-redux';
import { createBrowserHistory } from 'history';
import { ConnectedRouter } from 'react-router-redux';
import RedBox from 'redbox-react';
import { Route } from 'react-router-dom';
import { Frontload } from 'react-frontload';
import App from './containers/App';
import configureStore from './redux/store';
import withTracker from './withTracker';
// Get initial state from server-side rendering
const initialState = window.__INITIAL_STATE__;
const history = createBrowserHistory();
const store = configureStore(history, initialState);
const mountNode = document.getElementById('react-view');
const noServerRender = window.__noServerRender__;
if (process.env.NODE_ENV !== 'production') {
console.log(`[react-frontload] server rendering configured ${noServerRender ? 'off' : 'on'}`);
}
const renderApp = () =>
hydrate(
<AppContainer errorReporter={({ error }) => <RedBox error={error} />}>
<Provider store={store}>
<Frontload noServerRender={window.__noServerRender__}>
<ConnectedRouter onUpdate={() => window.scrollTo(0, 0)} history={history}>
<Route
component={withTracker(() => (
<App noServerRender={noServerRender} />
))}
/>
</ConnectedRouter>
</Frontload>
</Provider>
</AppContainer>,
mountNode,
);
// Enable hot reload by react-hot-loader
if (module.hot) {
const reRenderApp = () => {
try {
renderApp();
} catch (error) {
hydrate(<RedBox error={error} />, mountNode);
}
};
module.hot.accept('./containers/App', () => {
setImmediate(() => {
// Preventing the hot reloading error from react-router
unmountComponentAtNode(mountNode);
reRenderApp();
});
});
}
renderApp();
I've reconstructed your use case in codesandbox.io and the "transition" works fine. So maybe checking out my implementation might help you. However, I replaced the library import by a file import, so I don't know if that's the decisive factor of why it doesn't work without a whole page reload.
By the way, what do you mean exactly by "seamlessly"? Are there elements that stay on every page and should not be reloaded again when clicking on the link? This is like I implemented it in the sandbox where a static picture stays at the top on every page.
Check out the sandbox.
This is the example.js file
// This sandbox is realted to this post https://stackoverflow.com/q/59630138/965548
import React from "react";
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
import { Heading } from "./my-external-library.js";
export default function App() {
return (
<div>
<img
alt="flower from shutterstock"
src="https://image.shutterstock.com/image-photo/pink-flowers-blossom-on-blue-600w-1439541782.jpg"
/>
<Router>
<Route exact={true} path="/" render={Welcome} />
<Route path="/article/coolArticle" component={CoolArticleComponent} />
</Router>
</div>
);
}
const Welcome = () => {
const articleWithLinkProps = {
url: `/article/coolArticle`,
routerLink: Link
};
return (
<div>
<h1>This is a super fancy homepage ;)</h1>
<Heading withLinkProps={articleWithLinkProps} />
</div>
);
};
const CoolArticleComponent = () => (
<div>
<p>This is a handcrafted article component.</p>
<Link to="/">Back</Link>
</div>
);
And this is the my-external-library.js file:
import React from "react";
export const Heading = ({ withLinkProps }) => {
const RouterLink = withLinkProps.routerLink;
return <RouterLink to={withLinkProps.url}>Superlink</RouterLink>;
};

React Hooks Scroll to Element

I am looking to write a React hook with React 16.8.6 that will let me scroll to a particular HTML element section on click of a navigation item. I have a Navigation component that is a sibling of the sections rendered on the page.
Also when the page scrolls, I would like to update state of the App with that HTML section.
Navigation Component JSX
<ul class="nav>
<li><a>Section 1</a></li>
<li><a>Section 2</a></li>
</ul>
Sections in Home Page at App Level Component
<section className="section-1">Section 1</section>
<section className="section-2">Section 2</section>
Hooks
const [navItem, setNavItem] = React.useState(null);
const sectionRef = React.useRef(null);
// Scroll To Item
useEffect(() => {
console.log(sectionRef.current);
if (sectionRef.current) {
sectionRef.current.scrollToItem();
}
}, []);
If you don't mind using react-router-dom, then you can track history changes and update the scroll position to an HTML element's id via a hash history change. The advantage of this approach is you don't have to utilize state, nor utilize refs, and it can scale across the entire application (regardless of where the elements are located within the application's tree, you can scroll to them).
Working example:
https://fglet.codesandbox.io/ (demo)
https://codesandbox.io/s/fglet (source -- unfortunately, doesn't work within the codesandbox editor)
components/ScrollHandler (hook that listens to hash history changes, searches for elements that match the id located within the hash and, if it finds a matching element id, then it'll scroll to the element)
import { useEffect } from "react";
import PropTypes from "prop-types";
import { withRouter } from "react-router-dom";
const ScrollHandler = ({ location }) => {
useEffect(() => {
const element = document.getElementById(location.hash));
setTimeout(() => {
window.scrollTo({
behavior: element ? "smooth" : "auto",
top: element ? element.offsetTop : 0
});
}, 100);
}, [location]);
return null;
};
ScrollHandler.propTypes = {
location: PropTypes.shape({
pathname: PropTypes.string,
search: PropTypes.string,
hash: PropTypes.string,
state: PropTypes.any,
key: PropTypes.string
}).isRequired
};
export default withRouter(ScrollHandler);
components/Navigation (links to change url hash history location)
import React from "react";
import { Link } from "react-router-dom";
import List from "../List";
const Navigation = () => (
<List>
{[1, 2, 3, 4, 5].map(num => (
<li key={num}>
<Link to={`/#section${num}`}>Section {num}</Link>
</li>
))}
</List>
);
export default Navigation;
components/Sections (the Headline component contains the id that will be matched against)
import React from "react";
import Headline from "../Headline";
const Sections = () =>
[1, 2, 3, 4, 5].map(num => (
<Headline key={num} id={`#section${num}`}>
Section {num}
</Headline>
));
export default Sections;
index.js
import React from "react";
import { render } from "react-dom";
import { BrowserRouter } from "react-router-dom";
import Container from "./components/Container";
import Navigation from "./components/Navigation";
import Sections from "./components/Sections";
import ScrollHandler from "./components/ScrollHandler";
import "./styles.css";
const App = () => (
<BrowserRouter>
<Container>
<ScrollHandler />
<Navigation />
<Sections />
</Container>
</BrowserRouter>
);
render(<App />, document.getElementById("root"));
I am using React Router V6. Some things didn't work and were different. For instance, withRouter was deprecated. React router offered a solution if you need it (link).
My solution for V6:
Create a component WithRouter.jsx:
import { useLocation, useNavigate, useParams } from "react-router-dom";
function withRouter(Component) {
function ComponentWithRouterProp(props) {
let location = useLocation();
let navigate = useNavigate();
let params = useParams();
return <Component {...props} router={{ location, navigate, params }} />;
}
return ComponentWithRouterProp;
}
export default withRouter;
Create a component ScrollHandler.jsx
import { useEffect } from "react";
import WithRouter from "./WithRouter";
const ScrollHandler = ({ location }) => {
useEffect(() => {
const element = document.getElementById(location.hash.substring(1));
if (element) element.scrollIntoView();
}, [location]);
return null;
};
export default WithRouter(ScrollHandler);
In index.js I wrapped my <App/> comp with the BrowserRouter as Router like so:
<Router>
<App />
</Router>
Then in App.js, add the <ScrollHandler/> component:
<ScrollHandler location={location} />

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?
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>
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")
);
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.
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;
}
}
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.
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
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}>
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;
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.
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>
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])
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!
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]);
};
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
}
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.
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>
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.
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);
}, []);
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>
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
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
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".)
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);
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.
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
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" },
]
});
Using useEffect() - Solution for Functional Component
useEffect(() => {
window.history.scrollRestoration = 'manual';}, []);
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> ) }
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>
)
}
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>

Categories