React Router Scroll to Top on V6 - reactjs

When I change pages, the application is being kept at the same point it was on the previous page. I want to show the component from the top when I change pages. To achieve that, I am trying to implement React Router ScrollToTop.
I found the documentation and implemented it, but I am using react router v6, so it is a bit different.
https://v5.reactrouter.com/web/guides/scroll-restoration
Everything inside the ScrollToTop component doesn't get rendered, and I end up with a blank page.
App.js:
import { Router, Routes, Route } from "react-router-dom";
import './App.scss';
import Main from './pages/Main';
import Projects from './pages/Projects';
import NavBar from './components/NavBar';
import Footer from './components/Footer';
import ScrollToTop from './components/scrollToTop';
function App() {
return (
<div className="app" id="app">
<NavBar />
<div className='app-body'>
<Router>
<ScrollToTop>
<Routes>
<Route path="/portfolio" element={<Main />} />
<Route path="/portfolio/projects" element={<Projects />} />
</Routes>
</ScrollToTop>
</Router>
</div>
<Footer />
</div>
);
}
export default App;
ScrollToTop.js:
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;
}

As others have pointed out, you are wrapping your Routes component with the ScrollToTop component, but instead of editing it to render its implicit children prop I suggest converting it to a React hook, especially considering since it doesn't actually render anything, you want it to run as a side-effect of navigation.
function useScrollToTop() {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
}
...
function App() {
useScrollToTop();
return (
<div className="App">
<div className="app-body">
<NavBar />
<Routes>
<Route path="/portfolio" element={<Main />} />
<Route path="/portfolio/projects" element={<Projects />} />
</Routes>
</div>
</div>
);
}
This necessarily requires you to lift the Router higher in the ReactTree to wrap the App component so it has a routing context to use for the useScrollToTop hook.
const rootElement = document.getElementById("root");
ReactDOM.render(
<StrictMode>
<Router>
<App />
</Router>
</StrictMode>,
rootElement
);

You put the Routes component as a descendant of ScrollToTop component, so you should return children instead of null.
ScrollToTop.js:
import { useEffect } from "react";
import { useLocation } from "react-router-dom";
export default function ScrollToTop({ children }) {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return children;
}

The blank page is because you are returning null from <ScrollToTop> component. Instead if you return <></> or take the {children} prop and return that from <ScrollToTop >, it should work :)

Related

React.lazy and custom protectedRoute with router-react-dom, how do I get it to work?

I'm new to react and is trying out the React.lazy and Suspense imports, and I just have to say, I love them!!! My website went from 45% in performance up to 50-60% and that is without optimizing images! Google search results, here I come!
However, I have a problem, I don't know how to lazy load a component which is rendered in my custom ProtectedRoute and react-router-dom v5.
The lazy loading works and takes effect when I use the React-router-doms native Route, but when I want to load a protected component via one in my custom protected routes, nothing happens, no error message in console or on the website, just a white screen. I suspect there's some problem with the import and code being put in the wrong place.
APP
import React, { Suspense } from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import ProtectedRoute from "./pages/middleware/ProtectedRoute";
const Login = React.lazy(() => import("./pages/Login"));
const WebsiteCRUDs = React.lazy(() => import("./pages/WebsiteCRUDs"));
function App() {
return (
<div className="App">
<Router>
<Switch>
{/* This one works */}
<Suspense fallback={<div>Loading</div>}>
<Route exact path="/admin" component={Login} />
</Suspense>
{/* This one does NOT work */}
<Suspense fallback={<div>Loading</div>}>
<ProtectedRoute exact path="/admin/crud" component={WebsiteCRUDs} />
</Suspense>
</Switch>
</Router>
</div>
);
}
export default App;
ProtectedRoute:
import React from "react";
import { Route, Redirect } from "react-router-dom";
import { useEffect, useState } from "react";
const ProtectedRoute = ({ component: Component, ...rest }) => {
const [isAuth, setIsAuth] = useState(false);
const [isLoading, setIsLoading] = useState(true);
// Logic validation goes here with redirect if user is not auth.
return (
<Route
{...rest}
render={(props) =>
isLoading ? (
<h1>Checking Validation</h1>
) : isAuth ? (
<Component {...props} />
) : (
<Redirect
to={{ pathname: "/admin", state: { from: props.location } }}
/>
)
}
/>
);
};
export default ProtectedRoute;
Please try like this
import React, { Suspense } from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import ProtectedRoute from "./pages/middleware/ProtectedRoute";
const Login = React.lazy(() => import("./pages/Login"));
const WebsiteCRUDs = React.lazy(() => import("./pages/WebsiteCRUDs"));
function App() {
return (
<div className="App">
<Router>
<Switch>
<Suspense fallback={<div>Loading</div>}>
<Route exact path="/admin" component={Login} />
<ProtectedRoute exact path="/admin/crud" component={WebsiteCRUDs} />
</Suspense>
</Switch>
</Router>
</div>
);
}
export default App;

Uncaught TypeError: Cannot read properties of undefined (reading 'location')

I am getting this error message while accessing the location from useLocation() hook. Actually I am trying to add google analytics to my react app but I am getting this error message.
Uncaught TypeError: Cannot read properties of undefined (reading 'location')
useGaTracker hook code:
import { useEffect, useState } from "react"
import { useLocation } from "react-router-dom"
import ReactGA from "react-ga"
const useGaTracker = () => {
const location = useLocation()
const [initialized, setInitialized] = useState(false)
console.log(location)
useEffect(() => {
if (!window.location.href.includes("localhost")) {
ReactGA.initialize(process.env.REACT_APP_GOOGLE_ANALYTICS_KEY)
setInitialized(true)
}
}, [])
useEffect(() => {
if (initialized) {
ReactGA.pageview(location.pathname + location.search)
}
}, [initialized, location])
}
export default useGaTracker
App.js code
import React, { Suspense } from "react"
import { BrowserRouter, Switch, Route } from "react-router-dom"
import "./App.css"
import { Helmet } from "react-helmet"
import { AuthProvider } from "./context/AuthContext"
// custom components
import SideBar from "./components/layouts/SideBar"
import Navbar from "./components/layouts/Navbar"
import Footer from "./components/layouts/Footer"
import useGaTracker from "./hooks/useGaTracker"
// lazy loading components
const Dashboard = React.lazy(() => import("./components/dashboard/Dashboard"))
const App = () => {
useGaTracker()
return (
<BrowserRouter>
<Helmet>
<title>CODINGSPACE - Learn by Building Web and Mobile Apps</title>
</Helmet>
<AuthProvider>
<div className="relative grid min-h-screen md:grid-cols-layout-tablet xl:grid-cols-layout-desktop grid-rows-layout-desktop md:gap-6">
<Navbar />
<SideBar />
<Switch>
<Suspense fallback={<div>Loading..</div>}>
<Route exact path="/" component={Dashboard} />
</Suspense>
</Switch>
<Feedback />
<Footer />
</div>
</AuthProvider>
</BrowserRouter>
)
}
export default App
Anyone please help me with this
The useGaTracker hook is being used outside the BrowserRouter so there is no routing context above it in the ReactTree.
To Resolve, move the router to the component rendering App so there's a provided routing context. This may likely be the index.js file. (You'd typically wrap app with the AuthProvider here as well)
Example:
... other imports ...
import { BrowserRouter as Router } from 'react-router-dom';
import { AuthProvider } from "./context/AuthContext"
const rootElement = document.getElementById("root");
ReactDOM.render(
<StrictMode>
<AuthProvider>
<Router>
<App />
</Router>
</AuthProvider>
</StrictMode>,
rootElement
);
...
const App = () => {
useGaTracker(); // <-- has a routing context now!
return (
<>
<Helmet>
<title>CODINGSPACE - Learn by Building Web and Mobile Apps</title>
</Helmet>
<div className="relative grid min-h-screen md:grid-cols-layout-tablet xl:grid-cols-layout-desktop grid-rows-layout-desktop md:gap-6">
<Navbar />
<SideBar />
<Switch>
<Suspense fallback={<div>Loading..</div>}>
<Route exact path="/" component={Dashboard} />
</Suspense>
</Switch>
<Feedback />
<Footer />
</div>
</>
)
}

Header component doesn't get updated after route change

I am trying to understand why Header component doesn't get updated when I click Button.
I believe that the problem is that I am not calling with Router. But why then App.js doesn't re render when I switch routes?
import React, { useEffect } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import First from './First';
import Second from './Second';
import Third from './Third';
import Header from './Header';
function App() {
return (
<div>
<Header />
<Router>
<Switch>
<Route exact path={'/'} component={First} />
<Route exact path={'/first'} component={Second} />
<Route exact path={'/second'} component={Third} />
</Switch>
</Router>
</div>
);
}
export default App;
import React from 'react';
export default function First(props) {
console.log('🚀 ~ file: First.js ~ line 4 ~ First ~ props', props);
return (
<div>
First
<button
onClick={() => {
props.history.push({
pathname: '/second',
});
}}
>
Go to Second
</button>
</div>
);
}
so my condition here doesn't get fired when path changes. the reason is that component hasn't been called and old condition is still there
import React from 'react'
export default function Header() {
console.log(window.location.pathname);
const logger = window.location.pathname === '/third' ? (<div>This is second</div>) :
(<div>this is root</div>)
return logger
}
I know that I can call Header somewhere else, but what is problem in this showcase?
The Header component is being rendered outside the Router, so it's not rerendered or made aware of route changes.
I suggest moving the Header component into the Router and have it access the route props.
App
function App() {
return (
<div>
<Router>
<Header /> // <-- move into Router
<Switch>
<Route exact path={'/'} component={First} />
<Route exact path={'/first'} component={Second} />
<Route exact path={'/second'} component={Third} />
</Switch>
</Router>
</div>
);
}
Header
import { useLocation } from 'react-router-dom';
export default function Header() {
const location = useLocation();
console.log(location.pathname);
return location.pathname === '/third'
? <div>This is second</div>
: <div>this is root</div>;
}
Alternatively you could use the useRouteMatch hook:
import { useRouteMatch } from 'react-router-dom';
export default function Header() {
const match = useRouteMatch('/third');
return match
? <div>This is second</div>
: <div>this is root</div>;
}

How to implement ScrollToTop in Material UI

I followed the documentation on how to implement ScrollToTop with React Router but I am unable to get the function to work. I created a component ScrollToTop.js with the following code:
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;
}
In my App.js I add the wrapper after Router but all I get is a white screen whenever I call ScrollToTop. Here is a snippet from App.js.
<Router history={createBrowserHistory()}>
<AuthProvider>
<GlobalStyles />
<GoogleAnalytics />
<CookiesNotification />
<ScrollToTop>
{renderRoutes(routes)}
</ScrollToTop>
</AuthProvider>
</Router>
I am told to wrap <ScrollToTop around <App /> but that is not located in App.js but instead inside index.js in the root directory.
Your ScrollToTop component does its job. I've set up a working example here.
I have not changed ScrollToTop at all. The app looks like this:
function App() {
return (
<Router>
<ScrollToTop />
<Routes />
<Links />
</Router>
);
}
Now anytime the Router's pathname changes, the app will scroll to the top.

Send props to react router component from parent layout component

I have created a layout component called Main which sends user prop to its children component using React.cloneElement(children, { user: 'First Name'}), but unable to pass that user prop to Route component.
index.js
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router, Route } from "react-router-dom";
import Main from "./Main.jsx";
import Home from "./Home.jsx";
const App = () => (
<Router>
<Main>
<Route
path="/"
render={props => {
return <Home {...props} />;
}}
/>
</Main>
</Router>
);
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Main.jsx
import React from "react";
export default props => {
return <div>{React.cloneElement(props.children, { user: "Username" })}</div>;
};
Home.jsx
import React from "react";
export default props => {
const { user } = props;
return <div>User - {user}</div>;
};
But unable to get user prop in Home component. If I do not use Route then it gets passed to Home.
When I do this, then Home receives user prop.
<Main>
<Home />
</Main>
How can I get user prop and send it to component rendered by Route?
Codesandox link of the scenario - https://codesandbox.io/s/kmyrj0zr8o
Well, you have to wrap Route into own component. The idea is to catch props and pass it manually to Route's render.
Here is modified source: https://codesandbox.io/s/zx508v60yl
Wrapping Route (MyRoute.jsx)
import React from "react";
import { Route } from "react-router-dom";
export default class MyRoute extends React.Component {
render() {
var { Component, path, exact, passedProps } = this.props;
return (
<Route
path={path}
exact={exact}
render={props => <Component {...props} {...passedProps} />}
/>
);
}
}
Modifying Main.jsx
import React from "react";
export default props => {
return (
<div>
{React.cloneElement(props.children, {
passedProps: {
user: "Username"
}
})}
</div>
);
};
Then changing Route to MyRoute in index.js
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router, Route } from "react-router-dom";
import Main from "./Main.jsx";
import Home from "./Home.jsx";
import MyRoute from "./MyRoute.jsx";
const App = () => (
<Router>
<Main>
<MyRoute path="/" Component={Home} />
</Main>
</Router>
);
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
When you need to pass props to the router component, you need to use the render prop to do that, but since React.cloneElement will actually just pass the props to the Route component, you can write a wrapper around Route, something like
const RouteWrapper = ({render, exact, strict, path, ...rest}) => {
return (
<Route
exact={exact}
strict={strict}
path={path}
render={(function(routerProps) => {
return render(...routerProps, ...rest)
})()}
/>
)
}
Which you can now use like
const App = () => (
<Router>
<Main>
<RouteWrapper
path="/"
render={props => {
return <Home {...props} />;
}}
/>
</Main>
</Router>
);

Resources