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>
Related
I don't know the reason but if I try use the Navigate Hook to redirect to a specific path when my desktop changes mode, it dives into a sort of "infinite loop " in the browser:
history.ts:633 Throttling navigation to prevent the browser from
hanging. See https://crbug.com/1038223. Command line switch
--disable-ipc-flooding-protection can be used to bypass the protection
react-dom.development.js:86 Warning: Maximum update depth exceeded.
This can happen when a component calls setState inside useEffect, but
useEffect either doesn't have a dependency array, or one of the
dependencies changes on every render.
My App.js
import "./App.scss";
import React, { Fragment, useEffect } from "react";
import { useMediaQuery } from "./hooks/MediaQuery";
import Desktop from "./Pages/Desktop/Desktop";
import { Route, Routes, Navigate } from "react-router-dom";
function App() {
return (
<Fragment>
<div className="App">
<Routes>
<Route
exact
path="*"
element={
useMediaQuery(750) ? (
<Desktop replace to="/" />
) : (
<Navigate replace to="/m" />
)
}
/>
</Routes>
</div>
</Fragment>
);
}
export default App;
It seems the issue is an unconditional redirect loop is created in mobile views, from "*" to "/m" which is matched by "*" and the Navigate component is rendered again.
To stop this you might consider moving the redirect into a component lifecycle and only redirect if not already on "/m". Here's a basic implementation using a useEffect hook in the media query hook.
import { useNavigate, useMatch } from "react-router-dom";
import { useEffect } from "react";
const useMediaQueryRedirect = (width) => {
const navigate = useNavigate();
const isMobileMatch = useMatch("/m");
useEffect(() => {
const checkWidth = () => {
if (window.innerWidth < width && !isMobileMatch) {
navigate("/m");
}
if (window.innerWidth >= width && isMobileMatch) {
navigate("/");
}
};
window.addEventListener("resize", checkWidth, { passive: true });
return () => {
window.removeEventListener("resize", checkWidth, { passive: true });
};
}, [isMobileMatch, navigate, width]);
};
Create a layout route to use the hook and render the appropriate nested routes.
import { Route, Routes, Outlet } from "react-router-dom";
const MediaLayout = () => {
useMediaQueryRedirect(750);
return <Outlet />;
};
...
return (
...
<Routes>
<Route element={<MediaLayout />}>
<Route path="/*" element={/* Main component routes */} />
<Route path="/m/*" element={/* Mobile component routes */} />
</Route>
</Routes>
...
);
I'm using react-router-dom version 6.0.2 here and the "Render" props isn't working, every time I got to the url mentioned in the Path of my Route tag it keeps throwing me this error - "Matched leaf route at location "/addRecipe" does not have an element. This means it will render an with a null value by default resulting in an "empty" page.". Can someone please help me with this issue
import './App.css';
import Home from './components/Home';
import AddRecipe from './components/AddRecipe';
import items from './data';
import React, { useState } from 'react';
import {BrowserRouter as Router, Routes, Route} from 'react-router-dom';
const App = () => {
const [itemsList, setItemsList] = useState(items)
const addRecipe = (recipeToAdd) => {
setItemsList(itemsList.concat([recipeToAdd]));
}
const removeItem = (itemToRemove) => {
setItemsList(itemsList.filter(a => a!== itemToRemove))
}
return (
<Router>
<Routes>
<Route path="/addRecipe" render={ ({history}) => {
return (<AddRecipe onAddRecipe={(newRecipe) => {
addRecipe(newRecipe);
history.push('/');
} } />);
} } />
</Routes>
</Router>
);
}
export default App;
In react-router-dom version 6, you should use element prop for this.
I suggest your read their document on upgrading from version 5 where they explain the changes.
For your problem, you should write something like this:
<Route
path="/addRecipe"
element={
<AddRecipe
onAddRecipe={(newRecipe) => {
addRecipe(newRecipe);
history.push('/');
}
/>
}
/>
The Route component API changed significantly from version 5 to version 6, instead of component and render props there is a singular element prop that is passed a JSX literal instead of a reference to a React component (via component) or a function (via render).
There is also no longer route props (history, location, and match) and they are accessible only via the React hooks. On top of this RRDv6 also no longer surfaces the history object directly, instead abstracting it behind a navigate function, accessible via the useNavigate hook. If the AddRecipe component is a function component it should just access navigate directly from the hook. If it unable to do so then the solution is to create a wrapper component that can, and then render the AddRecipe component with the corrected onAddRecipe callback.
Example:
const AddRecipeWrapper = ({ addRecipe }) => {
const navigate = useNavigate();
return (
<AddRecipe
onAddRecipe={(newRecipe) => {
addRecipe(newRecipe);
navigate('/');
}}
/>
);
};
...
const App = () => {
const [itemsList, setItemsList] = useState(items);
const addRecipe = (recipeToAdd) => {
setItemsList(itemsList.concat([recipeToAdd]));
};
const removeItem = (itemToRemove) => {
setItemsList(itemsList.filter(a => a !== itemToRemove))
};
return (
<Router>
<Routes>
<Route
path="/addRecipe"
element={<AddRecipeWrapper addRecipe={addRecipe} />}
/>
</Routes>
</Router>
);
};
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>;
};
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.
I want 2 pages in my Chrome extension. For example: first(default) page with list of users and second with actions for this user.
I want to display second page by clicking on user(ClickableListItem in my case). I use React and React Router. Here the component in which I have:
class Resents extends React.Component {
constructor(props) {
super(props);
this.handleOnClick = this.handleOnClick.bind(this);
}
handleOnClick() {
console.log('navigate to next page');
const path = '/description-view';
browserHistory.push(path);
}
render() {
const ClickableListItem = clickableEnhance(ListItem);
return (
<div>
<List>
<ClickableListItem
primaryText="Donald Trump"
leftAvatar={<Avatar src="img/some-guy.jpg" />}
rightIcon={<ImageNavigateNext />}
onClick={this.handleOnClick}
/>
// some code missed for simplicity
</List>
</div>
);
}
}
I also tried to wrap ClickableListItem into Link component(from react-router) but it does nothing.
Maybe the thing is that Chrome Extensions haven`t their browserHistory... But I don`t see any errors in console...
What can I do for routing with React?
I know this post is old. Nevertheless, I'll leave my answer here just in case somebody still looking for it and want a quick answer to fix their existing router.
In my case, I get away with just switching from BrowserRouter to MemoryRouter. It works like charm without a need of additional memory package!
import { MemoryRouter as Router } from 'react-router-dom';
ReactDOM.render(
<React.StrictMode>
<Router>
<OptionsComponent />
</Router>
</React.StrictMode>,
document.querySelector('#root')
);
You can try other methods, that suits for you in the ReactRouter Documentation
While you wouldn't want to use the browser (or hash) history for your extension, you could use a memory history. A memory history replicates the browser history, but maintains its own history stack.
import { createMemoryHistory } from 'history'
const history = createMemoryHistory()
For an extension with only two pages, using React Router is overkill. It would be simpler to maintain a value in state describing which "page" to render and use a switch or if/else statements to only render the correct page component.
render() {
let page = null
switch (this.state.page) {
case 'home':
page = <Home />
break
case 'user':
page = <User />
break
}
return page
}
I solved this problem by using single routes instead of nested. The problem was in another place...
Also, I created an issue: https://github.com/ReactTraining/react-router/issues/4309
This is a very lightweight solution I just found. I just tried it - simple and performant: react-chrome-extension-router
I just had to use createMemoryHistory instead of createBrowserHistory:
import React from "react";
import ReactDOM from "react-dom";
import { Router, Switch, Route, Link } from "react-router-dom";
import { createMemoryHistory } from "history";
import Page1 from "./Page1";
import Page2 from "./Page2";
const history = createMemoryHistory();
const App: React.FC<{}> = () => {
return (
<Router history={history}>
<Switch>
<Route exact path="/">
<Page1 />
</Route>
<Route path="/page2">
<Page2 />
</Route>
</Switch>
</Router>
);
};
const root = document.createElement("div");
document.body.appendChild(root);
ReactDOM.render(<App />, root);
import React from "react";
import { useHistory } from "react-router-dom";
const Page1 = () => {
const history = useHistory();
return (
<button onClick={() => history.push("/page2")}>Navigate to Page 2</button>
);
};
export default Page1;
A modern lightweight option has presented itself with the package wouter.
You can create a custom hook to change route based on the hash.
see wouter docs.
import { useState, useEffect } from "react";
import { Router, Route } from "wouter";
// returns the current hash location in a normalized form
// (excluding the leading '#' symbol)
const currentLocation = () => {
return window.location.hash.replace(/^#/, "") || "/";
};
const navigate = (to) => (window.location.hash = to);
const useHashLocation = () => {
const [loc, setLoc] = useState(currentLocation());
useEffect(() => {
// this function is called whenever the hash changes
const handler = () => setLoc(currentLocation());
// subscribe to hash changes
window.addEventListener("hashchange", handler);
return () => window.removeEventListener("hashchange", handler);
}, []);
return [loc, navigate];
};
const App = () => (
<Router hook={useHashLocation}>
<Route path="/about" component={About} />
...
</Router>
);