useEffect is getting in a loop - reactjs

I am trying to implement in my app.js a simple Protect Route Middleware.
If the user is authenticated, he can not go to "/login" and "/". I also made a simple function in app.js, which is checking if the user is not authenticated. If he is not authenticated, he will be redirected from "/home" to "/login". Unfortunately my website does not stop to refresh if I am doing that.
For example : If am not authenticated and I trying to visit "/home", I will be redirect to "/login", but then go in refresh loop ! :(
app.js
function App() {
const authenticated = useSelector((state) => state.user.authenticated)
const dispatch = useDispatch()
useEffect(() => {
if (!authenticated) {
window.location.href = '/login'
}
}, [
// I tried every possible combination
])
return (
<div>
<Router>
<Switch>
<Route exact path='/home' component={HomePage} />
<ProtectedRoute exact path='/login' component={LoginPage} />
<ProtectedRoute exact path='/' component={LoginPage} />
</Switch>
</Router>
</div>
)
}
ProtectedToute.js
import React from 'react'
import { useSelector } from 'react-redux'
import { Route, Redirect } from 'react-router-dom'
const ProtectedRoute = ({ component: Component, ...rest }) => {
const authenticated = useSelector((state) => state.user.authenticated)
console.log('Route', authenticated)
return (
<Route
{...rest}
render={(props) => {
if (authenticated) {
return <Redirect to='/home' />
} else {
return (
<Component {...props} />
)
}
}}
/>
)
}
export default ProtectedRoute

Related

How to prevent a route from registering before jwt verifies react

today I'm having an issue where react loads the route before my API verifies that the user's JWT token is valid. When using EJS I could pass in a middleware to the route and the middleware would not contain the next() parameter. As a result the server wouldn't render the EJS which is exactly what I want to achieve with react. Also is it possible to make useNavigate not reload when navigating the that specific route?
My routes in App.js currently look like this:
<Route element={<ProtectedRoute access={access}></ProtectedRoute>}>
<Route
path="/login"
exact
element={<Login login={login} access={access}></Login>}
></Route>
<Route
path="/signup"
exact
element={<Signup signup={signup} access={access}></Signup>}
></Route>
<Route
path="/forgot-password"
exact
element={<ForgotPassword access={access}></ForgotPassword>}
></Route>
<Route
path="/reset-password"
exact
element={<ResetPassword access={access}></ResetPassword>}
></Route>
</Route>;
The access function looks like this:
const access = async (token) => {
return await axios.post(
"http://localhost:5000/access",
{},
{ headers: { Authorization: `Bearer ${token}` } }
);
};
The protected route component looks like this:
import { useState, useContext } from "react";
import { useLocation, useNavigate, Outlet } from "react-router-dom";
import AuthContext from "../Context/AuthProvider";
const ProtectedRoute = ({ access }) => {
const [authorized, setAuthorized] = useState(false);
const { auth } = useContext(AuthContext);
const navigate = useNavigate();
const authorize = async () => {
try {
await access(auth.accessToken);
setAuthorized(true);
} catch (err) {
setAuthorized(false);
}
};
authorize();
if (authorized) {
navigate('/');
} else {
return <Outlet></Outlet>;
}
};
export default ProtectedRoute;
When I use this code my login component renders a bit before the code navigates back to the home page, how do I make the login component not render at all and just make it stay on the home page?
Issue
The ProtectedRoute component's initial authorized state masks the confirmed unauthenticated state, and since the component doesn't wait for authentication confirmation it happily and incorrectly redirects to "/".
The ProtectedRoute component incorrectly issues a navigation action as an unintentional side-effect via the navigate function and doesn't return valid JSX in the unauthenticated case. Use the Navigate component instead.
If the user is authorized the ProtectedRoute should render the Outlet for a protected route to be rendered into, and only redirect to login if unauthorized.
Solution
The ProtectedRoute component should use an indeterminant initial authorized state that doesn't match either the authenticated or unauthenticated state, and wait for the auth status to be confirmed before rendering either the Outlet or Navigate components.
Example:
import { useState, useContext } from "react";
import { useLocation, Navigate, Outlet } from "react-router-dom";
import AuthContext from "../Context/AuthProvider";
const ProtectedRoute = ({ access }) => {
const location = useLocation();
const [authorized, setAuthorized] = useState(); // initially undefined!
const { auth } = useContext(AuthContext);
useEffect(() => {
const authorize = async () => {
try {
await access(auth.accessToken);
setAuthorized(true);
} catch (err) {
setAuthorized(false);
}
};
authorize();
}, []);
if (authorized === undefined) {
return null; // or loading indicator/spinner/etc
}
return authorized
? <Outlet />
: <Navigate to="/login" replace state={{ from: location }} />;
};
Move the login route outside the ProtectedRoute layout route.
<Routes>
<Route
path="/login"
element={<Login login={login} access={access} />}
/>
<Route
path="/signup"
element={<Signup signup={signup} access={access} />}
/>
<Route
path="/forgot-password"
element={<ForgotPassword access={access} />}
/>
<Route
path="/reset-password"
element={<ResetPassword access={access} />}
/>
... other unprotected routes ...
<Route element={<ProtectedRoute access={access} />}>
... other protected routes ...
</Route>
</Routes>
To protect the login/signup/forgot/reset/etc routes
Create an AnonymousRoute component that inverts the Outlet and Navigate components on the authentication status. This time authenticated users get redirected off the route.
const AnonymousRoute = ({ access }) => {
const [authorized, setAuthorized] = useState(); // initially undefined!
const { auth } = useContext(AuthContext);
useEffect(() => {
const authorize = async () => {
try {
await access(auth.accessToken);
setAuthorized(true);
} catch (err) {
setAuthorized(false);
}
};
authorize();
}, []);
if (authorized === undefined) {
return null; // or loading indicator/spinner/etc
}
return authorized
? <Navigate to="/" replace />
: <Outlet />;
};
...
<Routes>
<Route element={<AnonymousRoute access={access} />}>
<Route path="/login" element={<Login login={login} access={access} />} />
<Route path="/signup" element={<Signup signup={signup} access={access} />} />
<Route path="/forgot-password" element={<ForgotPassword access={access} />} />
<Route path="/reset-password" element={<ResetPassword access={access} />} />
... other protected anonymous routes ...
</Route>
... unprotected routes ...
<Route element={<ProtectedRoute access={access} />}>
... other protected authenticated routes ...
</Route>
</Routes>

How to check if user is logged by localStorage and Redirect according to it?

I am new to localStorage and React Router, and my goal is:
Redirect user to the "/dashboard" when he is logged in, and Redirect back to '/home' when he is logged out. Also, of course, not allowing him to go to the 'dashboard' if he is not logged in. For some reason my code in App.js not working:
function App() {
let userLogged;
useEffect(() => {
function checkUserData() {
userLogged = localStorage.getItem("userLogged");
}
window.addEventListener("storage", checkUserData);
return () => {
window.removeEventListener("storage", checkUserData);
};
}, []);
return (
<div className="App">
<React.StrictMode>
<Router>
<Routes>
<Route path="/" element={<Home />} />
{userLogged ? (
<Route path={"/dashboard"} element={<Dashboard />} />
) : (
<Route path={"/home"} element={<Home />} />
)}
</Routes>
</Router>
</React.StrictMode>
</div>
);
}
export default App;
I set it in the home and dashboard pages by localStorage.setItem('userLogged', false) and localStorage.setItem('userLogged', true)
You can only listen to changes in localStorage from other window/browser contexts, not from within the same browser/window context. Here it's expected the window knows its own state. In this case, you actually need some React state.
Convert the userLogged to a React state variable and use a useEffect hook to initialize and persist the userLogged state to/from localStorage. Instead of conditionally rendering Route components, create a wrapper component to read the userLogged value from localStorage and conditionally render an Outlet for nested/wrapped routes or a Navigate component to redirect to your auth route to login.
Example:
import { Navigate, Outlet, useLocation } from 'react-router-dom';
const AuthWrapper = () => {
const location = useLocation(); // current location
const userLogged = JSON.parse(localStorage.getItem("userLogged"));
return userLogged
? <Outlet />
: (
<Navigate
to="/"
replace
state={{ from: location }} // <-- pass location in route state
/>
);
};
...
function App() {
const [userLogged, setUserLogged] = useState(
JSON.parse(localStorage.getItem("userLogged"))
);
useEffect(() => {
localStorage.setItem("userLogged", JSON.stringify(userLogged));
}, [userLogged]);
const logIn = () => setUserLogged(true);
const logOut = () => setUserLogged(false);
return (
<div className="App">
<React.StrictMode>
<Router>
<Routes>
<Route path="/" element={<Home logIn={logIn} />} />
<Route path={"/home"} element={<Home logIn={logIn} />} />
<Route element={<AuthWrapper />}>
<Route path={"/dashboard"} element={<Dashboard />} />
</Route>
</Routes>
</Router>
</React.StrictMode>
</div>
);
}
export default App;
Home
import { useLocation, useNavigate } from 'react-router-dom';
const Home = ({ logIn }) => {
const { state } = useLocation();
const navigate = useNavigate();
const loginHandler = () => {
// authentication logic
if (/* success */) {
const { from } = state || {};
// callback to update state
logIn();
// redirect back to protected route being accessed
navigate(from.pathname, { replace: true });
}
};
...
};
You can render both route and use Navigate component to redirect. Like this -
// [...]
<Route path={"/dashboard"} element={<Dashboard />} />
<Route path={"/home"} element={<Home />} />
{
userLogged ?
<Navigate to="/dashboard" /> :
<Navigate to="/home" />
}
// other routes
Whenever you logout, you need to manually redirect to the desired page using useNavigate hook.

Using react-router with firebase authentication

My main App component keeps track of the user that is currently logged in via the firebase onAuthStateChanged callback, which I can then use to redirect the user to the /login route if the user object is null. This works fine, but if you navigate to a different route while on the login page, you don't get redirected back, which causes errors as other routes require you to be logged in to function properly. Here is the code:
export function App() {
const auth = firebase.auth();
const [user, setUser] = useState(null);
useEffect(()=>{
auth.onAuthStateChanged(()=> {
setUser(auth.currentUser);
})
}, []);
return (
<BrowserRouter>
<Switch>
<Route path="/login" exact component={LoginPage}/>
<Route path="/" exact component={HomePage}/>
{!user ? <Redirect to="/login"/> : null}
</Switch>
</BrowserRouter>
);
}
I've tried moving !user ? <Redirect to="/login"/> to the top of the Switch component, but that just makes it so you log out every time the page is refreshed. Any ideas on how to solve this? Thanks.
Why not recompose your Route element to have private routers and public routes? Private routes will be those requiring authentication and public once will not require it. When someone tries to access a private route without authentication, they will automatically be sent away.
Create an element called PrivateRoute and put your firebase auth inside it. Example:
const PrivateRoute = ({children, ...props}) => {
const auth = firebase.auth();
const [user, setUser] = useState(null);
useEffect(()=>{
auth.onAuthStateChanged(()=> {
setUser(auth.currentUser);
})
}, []);
return (
<Route {...props} render={() => {
return valid === null ?
<div>Some kind of loader/spinner here...</div>
:
user ?
children
:
<Redirect to='/login' />
}} />
)
}
Then in your App, use it like so:
return (
<BrowserRouter>
<Switch>
<PrivateRoute exact path="/">
<HomePage />
</PrivateRoute>
<Route exact path="/login" component={LoginPage} />
</Switch>
</BrowserRouter>
);
This will redirect anybody trying to access / to /login if they are not authenticated.
Later any route you create can be wrapped like this if it requires authentication.
I am using the following approach and it works fine (just copy my existing project that works):
import React, {useState, useEffect} from 'react'
import {BrowserRouter as Router, Switch, Route, Redirect} from "react-router-dom"
import {connect} from "react-redux"
import useAuth from './hooks/useAuth'
import styles from './styles.js'
import Landing from './components/Landing'
import Login from './components/Login'
import Logout from './components/Logout'
import Post from './components/Post'
import Users from './components/Users'
import User from './components/User'
import Signup from './components/Signup'
import Profile from './components/Profile'
import AddSocieta from './components/AddSocieta'
import Constructor from './components/Constructor'
const mapStateToProps = state => ({
...state
});
function ConnectedApp() {
const [dimension, setDimention] = useState({windowWidth: 0, windowHeight: 0})
const currentStyles = {
...styles,
showFooterMenuText: styles.showFooterMenuText(dimension),
showSidebar: styles.showSidebar(dimension),
topMenuCollapsed: styles.topMenuCollapsed(dimension),
topMenuHeight: styles.topMenuHeight(dimension),
paddingLeftRight: styles.paddingLeftRight(dimension),
fullScreenMenuFontSize: styles.fullScreenMenuFontSize(dimension),
showSubLogoText: styles.showSubLogoText(dimension),
roundedImageSize: styles.roundedImageSize(dimension)
};
const [auth, profile] = useAuth()
const [isLoggedIn, setIsLoggedIn] = useState(false)
useEffect(() => {
if (auth && auth.uid) {
setIsLoggedIn(true)
} else {
setIsLoggedIn(false)
}
updateDimensions();
window.addEventListener("resize", updateDimensions);
return function cleanup() {
window.removeEventListener("resize", updateDimensions);
}
}, [auth, profile]);
function updateDimensions() {
let windowWidth = typeof window !== "undefined"
? window.innerWidth
: 0;
let windowHeight = typeof window !== "undefined"
? window.innerHeight
: 0;
setDimention({windowWidth, windowHeight});
}
return (<Router>
<Redirect to="/app/gare"/>
<div className="App">
<Switch>
<Route path="/constructor"><Constructor styles={currentStyles}/></Route>
<Route path="/post"><Post/></Route>
<Route path="/login"><Login styles={currentStyles}/></Route>
<Route path="/logout"><Logout styles={currentStyles}/></Route>
<Route path="/users"><Users styles={currentStyles}/></Route>
<Route path="/user/:uid"><User styles={currentStyles}/></Route>
<Route path="/app"><Landing styles={currentStyles}/></Route>
<Route path="/signup" render={isLoggedIn
? () => <Redirect to="/app/gare"/>
: () => <Signup styles={currentStyles}/>}/>
<Route path="/profile" render={isLoggedIn
? () => <Profile styles={currentStyles}/>
: () => <Redirect to="/login"/>}/>
<Route path="/add-societa"><AddSocieta styles={currentStyles}/></Route>
</Switch>
</div>
</Router>);
}
const App = connect(mapStateToProps)(ConnectedApp)
export default App;

Conditional Route not rendering the redirected Layout

In AppRouter, I have a conditional route with redirect for <AdminLayout/>.
relevant snippet:
<Route
exact
path="/admin"
strict
render={(props) => <AdminLayout {...props} />}
>
{loggedIn ? <Redirect to="/admin/summary" /> : <Login />}
</Route>
If loggedIn is true then, redirect to /admin/summary else redirect it back to <Login/>
The problem is: it is only changing the URL but not rendering the <AdminLayout/>.
Not sure where I am going wrong and what I am missing.
UPDATED PrivateRoute and AppRouter below
AppRouter
import React, { useEffect } from "react";
import { Router, Route, Switch, Redirect } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { createBrowserHistory } from "history";
import { alertActions } from "../actions";
import { history } from "../helpers";
import AdminLayout from "layouts/Admin/Admin.js";
import AuthLayout from "layouts/Auth/Auth.js";
import ResetPassword from "../components/pages/reset-password/ResetPassword";
import MailReset from "../components/pages/reset-password/MailReset";
import PrivateRoute from "../routes/PrivateRoute";
import Dashboard from "views/Dashboard";
const hist = createBrowserHistory();
const AppRouter = () => {
const alert = useSelector((state) => state.alert);
const dispatch = useDispatch();
useEffect(() => {
history.listen((location, action) => {
// clear alert on location change
dispatch(alertActions.clear());
});
}, []);
return (
<Router history={hist}>
<Switch>
{/* <Route path="/admin" render={(props) => <AdminLayout {...props} />} /> */}
<PrivateRoute exact path="/admin">
<Dashboard />
</PrivateRoute>
<Route
path="/auth/login"
render={(props) => <AuthLayout {...props} />}
/>
<Route exact path="/auth/forgotPassword" component={ResetPassword} />
<Route exact path="/auth/mail_reset" component={MailReset} />
<Redirect from="*" to="/auth/login" />
</Switch>
</Router>
);
};
export default AppRouter;
PrivateRoute
import React from "react";
import { Route, Redirect } from "react-router-dom";
import AdminLayout from "../layouts/Admin/Admin";
function PrivateRoute({ component: Component, roles, ...rest }) {
console.log("rest pvt route", ...rest);
return (
<Route
{...rest}
render={(props) => {
console.log("propsssss", props);
// if (!localStorage.getItem('userid')) {
if (!localStorage.getItem("access_token")) {
// not logged in so redirect to login page with the return url
return (
<Redirect
to={{ pathname: "/auth/login", state: { from: props.location } }}
/>
);
}
// logged in so return component
return <AdminLayout {...props} />;
}}
/>
);
}
export default { PrivateRoute };
So trying to explain what its is wrong:
You are setting rendering child and render props that's why children props takes priority here:
<Route
exact
path="/admin"
render={(props) => <AdminLayout {...props} />}
>
{loggedIn ? <Redirect to="/admin/summary" /> : <Login />}
</Route>
Your private route is correct but need to add your layout as well:
return <AdminLayout {...props} /><Component {...props} /></AdminLayout/>;
Inside app route you need to import PrivateRoute component it will look like this:
import PrivateRoute from './PrivateRoute';
const AppRouter = () => {
const alert = useSelector((state) => state.alert);
const loggedIn = useSelector((state) => state.authentication.loggedIn);
const dispatch = useDispatch();
useEffect(() => {
history.listen((location, action) => {
// clear alert on location change
dispatch(alertActions.clear());
});
}, []);
return (
<Router history={hist}>
<Switch>
<PrivateRoute exact path='/admin'>
<YOUR AUTH COMPONENT WHICH YOU WANT TO RENDER />
</PrivateRoute>
<Route
path='/auth/login'
render={(props) => <AuthLayout {...props} />}
/>
<Route exact path='/auth/forgotPassword' component={ResetPassword} />
<Route exact path='/auth/mail_reset' component={MailReset} />
<Redirect from='*' to='/auth/login' />
</Switch>
</Router>
);
};
Here I created demo code of this. Take reference from it: https://codesandbox.io/s/react-router-redirects-auth-forked-6q6o4?file=/example.js

React router is not redirecting based on condition

I am creating a school management system using ReactJS. In this I want to redirect the login page to dashboard when isLoggedIn props changes from false to true. From the console I make sure it is changing but I can't perform a conditional routing based on this props. Props is changing but not redirecting.
import { BrowserRouter, Switch, Route, Redirect } from 'react-router-dom'
...
class App extends Component {
renderRoutes = () => {
const { isLoggedIn } = this.props.auth
return isLoggedIn ?
<Redirect to='dashboard' /> :
<Redirect to='/' />
}
render() {
return (
<BrowserRouter>
<Navbar />
<Switch>
<Route exact path="/" component={Authenticate} />
<Route path="/dashboard" component={DashboardTeachers} />
{this.renderRoutes()}
</Switch>
</BrowserRouter>
)
}
}
I want to redirect to '/dashboard' after successful login and redirect to '/' after successful logout
You can wrap private routes inside separate component and redirect from there.
Create a PrivateRoute component first:
function PrivateRoute({ auth, children, ...rest }) {
const { isLoggedIn } = auth;
return (
<Route
{...rest}
render={({ location }) =>
isLoggedIn ? (
children
) : (
<Redirect
to="/login"
/>
)
}
/>
);
}
Replace your code
<Route path="/dashboard" component={DashboardTeachers} />
{this.renderRoutes()}
with this
<PrivateRoute auth={auth}>
<Route path="/dashboard" component={DashboardTeachers} />
</PrivateRoute>
And in your login component, redirect after successful authentication
if (!isLoggedIn) {
return (
// your login ui
);
}
// redirect to dashboard if login is false
return <Redirect to="/dashboard" />;

Resources