React-router: ProtectedRoute authentication logic not working - javascript

I am trying to implement protected routes that are only accessible after the user logs in.
Basically, there are two routes: /login Login component (public) and / Dashboard component (protected). After the user clicks on the Login button in /login, an API is called which returns an accessToken, which is then stored in localStorage. The protectedRoute HOC checks if the token is present in the localStorage or not and redirects the user accordingly.
After clicking on the Login button it just redirects back to the login page instead of taking user to Dashboard. Not sure what is wrong with the logic.
asyncLocalStorage is just a helper method for promise based localStorage operations.
App.js
import { useEffect, useState } from "react";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import Dashboard from "./Dashboard";
import Login from "./Login";
import ProtectedRoute from "./ProtectedRoute";
import { asyncLocalStorage } from "./asyncLocalStorage";
function App() {
const [auth, setAuth] = useState(false);
useEffect(() => {
const getToken = async () => {
const token = await asyncLocalStorage.getItem("accessToken");
if (token) setAuth(true);
};
getToken();
}, []);
return (
<div className="app">
<Router>
<Switch>
<ProtectedRoute exact path="/" isLoggedIn={auth} redirectTo="/login">
<Dashboard />
</ProtectedRoute>
<Route exact path="/login">
<Login />
</Route>
</Switch>
</Router>
</div>
);
}
export default App;
ProtectedRoute.js
import { Route, Redirect } from "react-router-dom";
const ProtectedRoute = ({ children, isLoggedIn, redirectTo, ...rest }) => {
return (
<Route
{...rest}
render={() => {
return isLoggedIn ? children : <Redirect to={redirectTo} />;
}}
/>
);
};
export default ProtectedRoute;
Dashboard.js
const Dashboard = () => {
return (
<div>
<h3>Dashboard</h3>
<button
onClick={() => {
localStorage.removeItem("accessToken");
window.location.href = "/login";
}}
>
Logout
</button>
</div>
);
};
export default Dashboard;
Login.js
import { asyncLocalStorage } from "./asyncLocalStorage";
const Login = () => {
return (
<div>
<h3>Login</h3>
<button
onClick={async () => {
// Making an API call here and storing the accessToken in localStorage.
await asyncLocalStorage.setItem(
"accessToken",
"SOME_TOKEN_FROM_API_RES"
);
window.location.href = "/";
}}
>
Login
</button>
</div>
);
};
export default Login;
asyncLocalStorage.js
export const asyncLocalStorage = {
setItem: (key, value) => {
Promise.resolve(localStorage.setItem(key, value));
},
getItem: (key) => {
return Promise.resolve(localStorage.getItem(key));
}
};

Problem: auth State Never Updated
Your ProtectedRoute component relies on the value of auth from the useState in App in order to determine whether the user is logged in or not. You set this value through a useEffect hook which runs once when the App is mounted. This hook is never run again and setAuth is never called anywhere else. If the user is logged out when App first mounts then the ProtectedRoute will always receive isLoggedIn={false} even after the user has logged in and the local storage has been set.
Solution: call setAuth from Login
A typical setup would check localStorage from the ProtectedRoute, but I don't think that will work with the async setup that you have here.
I propose that you pass both isLoggedIn and setAuth to the Login component.
<Login isLoggedIn={!!auth} setAuth={setAuth}/>
In the Login component, we redirect to home whenever we get a true value of isLoggedIn. If a users tries to go to "/login" when they are already logged in, they'll get redirected back. When the log in by clicking the button, they'll get redirected when the value of auth changes to true. So we need to change that value.
The onClick handler for the button will await setting the token in storage and then call setAuth(token). This will trigger the redirection.
import { asyncLocalStorage } from "./asyncLocalStorage";
import {Redirect} from "react-router-dom";
const Login = ({isLoggedIn, setAuth}) => {
if ( isLoggedIn ) {
return <Redirect to="/"/>
}
return (
<div>
<h3>Login</h3>
<button
onClick={async () => {
// Making an API call here and storing the accessToken in localStorage.
const token = "SOME_TOKEN_FROM_API_RES";
await asyncLocalStorage.setItem(
"accessToken",
token
);
setAuth(token);
}}
>
Login
</button>
</div>
);
};
export default Login;

Related

Uncaught Error: Too many re-renders. React limits the number of renders to prevent an infinite loop - ProtectedRoutes Component

I am attempting to create a ProtectedRoutes component, however, I seem to have created an infinite loop somewhere that I can't seem to figure out. I'm a beginner.
It should check if there is a cookie stored, and if so, go to the component. If not, it should navigate back to the main page.
ProtectedRoutes.js
import React, { Component, useState } from "react";
import { Route, Navigate } from "react-router-dom";
import Cookies from "universal-cookie";
const cookies = new Cookies();
export default function ProtectedRoutes({component: Component, ...rest}) {
const [auth, setAuth] = useState(false);
//get cookie from browser if logged in
const token = cookies.get("TOKEN");
if (token) {
setAuth(true);
};
return auth ? <Component /> : <Navigate to="/" />
}
App.js
import { Container, Col, Row } from "react-bootstrap";
import "./App.css";
import Register from "./Register";
import Login from "./Login";
import { Routes, Route } from "react-router-dom";
import Account from "./Account";
import FreeComponent from "./FreeComponent";
import AuthComponent from "./AuthComponent";
import Private from "./ProtectedRoutes";
import ProtectedRoutes from "./ProtectedRoutes";
function App() {
return (
<Container>
<Row>
<Col className="text-center">
<h1 className="header">React Authentication Tutorial</h1>
<section id="navigation">
Home
Free Component
Auth Component
</section>
</Col>
</Row>
{/* Routes */ }
<Routes>
<Route exact path="/" element={ <Account /> } />
<Route exact path="/free" element={ <FreeComponent /> } />
<Route path="/auth" element={<ProtectedRoutes component={AuthComponent} />} />
</Routes>
</Container>
);
}
export default App;
AuthComponent.js
import React from 'react';
export default function AuthComponent() {
return (
<div>
<h1 className="text-center">Auth Component</h1>
</div>
);
}
Yow problem Is heaw.
export default function ProtectedRoutes({component: Component, ...rest}) {
const [auth, setAuth] = useState(false);
//get cookie from browser if logged in
const token = cookies.get("TOKEN");
if (token) {
setAuth(true);
};
return auth ? <Component /> : <Navigate to="/" />
}
You need yo put setAuth in a useEffect
export default function ProtectedRoutes({component: Component, ...rest}) {
const [auth, setAuth] = useState(false);
React.useEffect(()=>{
const token = cookies.get("TOKEN");
if (token) {
setAuth(true);
}
},[auth]);
return auth ? <Component /> : <Navigate to="/" />
}
In ProtectedRoutes component, you're setting a state (setAuth in this case) directly inside the component, this is what happens when you do that:
React re-renders a component every time a state change is detected thus when you wrote
export default function ProtectedRoutes({component: Component, ...rest}) {
...
if (token) {
setAuth(true);
};
...
}
you're running setAuth(sets a new state true) every time the component renders(re-renders) and in turn, re-rendering the component every time because of the state change which is why have an infinite loop there.
, this works like this:
It runs everytime a dependency changes(passed as an array), and when you pass an empty array it runs just twice - when the component mounts(renders the first time) and when it unmounts.
What you need to do is to pass an empty array as a dependency as below
import { useEffect } from 'react'
export default function ProtectedRoutes({component: Component, ...rest}) {
...
useEffect(() => {
if (token) {
setAuth(true);
};
}, [])
...
}
this setAuth just once when the component mounts

How to stop already logged in user to go back to the sign-in page in React.js

Here, I'm trying to stop the user to go back to the login page if the user is currently signed in, the first line in the render function is causing an error
Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
But I'm not calling setState here, I'm just calling a function from another module to check if it returns a user or not
render() {
if (auth.getCurrentUser()) return <Redirect to="/" />;
return (
<div id="loginWrapper">
<h3>Login</h3>
<ToastContainer />
<form onSubmit={this.handleSubmit}>
{this.renderInput("username", "Username")}
{this.renderInput("password", "Password", "password")}
{this.renderButton("Login")}
</form>
</div>
);
}
and here is my auth module from where the function is being called,
import jwtDecode from "jwt-decode";
export function getCurrentUser() {
try {
const jwt = localStorage.getItem("token");
return jwtDecode(jwt);
} catch {
return null;
}
}
export default {
getCurrentUser,
};
use ternary condition with jwt token on your route which may help you to check the user is logged in or not also you can make your route private. ty
You could create a ProtectedRoute component that redirects based on auth status.
import React from 'react';
import { Route } from 'react-router-dom';
const ProtectedRoute = ({ component: Component, ...restProps }) => {
return (
<Route {...restProps} render={
props => {
if (!auth.getCurrentUser()) {
return <Component {...rest} {...props} />
} else {
return <Redirect to={
{
pathname: '/',
state: {
from: props.location
}
}
} />
}
}
} />
)
}
export default ProtectedRoute;
You can then call the ProtectedRoute component on the login page thus:
<ProtectedRoute path='/login' component={LoginPage} />

Problems with react router trying to redirect users

i'm working on a MERNG app, and i have a problem loging out the user.
So, i have a route, that will take you to a page where the url has /profile/userId, the userID is given by the localStorage since i'm taking that value from there, and, when i logout i remove the token from the localStorage, so, if there's no values, it should take you login page.
Now, the problem is that, when i login, and get to the /profile/userId page, and i refresh the page with f5, if i logout, it won't let me do it, the redirecting doesn't work, it only works if i don't refresh the page, and that's a big issue for my app, and it's actually weird.
Maybe it's a problem with my code, i don't know that well how to use Redirect, so if you can help me with this, you're the greatest !
So this is the code
Routes with the redirect
import React from "react";
import "./App.css";
import {
BrowserRouter as Router,
Switch,
Route,
Redirect
} from "react-router-dom";
import Auth from "./website/Auth";
import SocialMedia from "./website/SocialMedia";
import SingleUser from "./website/SingleUser";
function App() {
const logIn = JSON.parse(localStorage.getItem("token"));
return (
<Router>
<Switch>
<Route exact path="/">
{logIn ? <Redirect to={`/profile/${logIn.data.id}`} /> : <Auth />}
</Route>
<Route exact path="/socialmedia" component={SocialMedia} />
<Route exact path="/profile/:id" component={SingleUser} />
</Switch>
</Router>
);
}
export default App;
To logout
import React from "react";
import { useHistory } from "react-router-dom";
import { useDispatch } from "react-redux";
const SingleUser = () => {
const history = useHistory();
const dispatch = useDispatch();
const userData = JSON.parse(localStorage.getItem("token"));
const logout = () => {
dispatch({ type: "LOGOUT" });
if (!userData) {
history.push("/");
}
};
return <div onClick={logout}>Single User</div>;
};
export default SingleUser;
My reducer where i store the token in the localStorage and remove it with logout action
const reducer = (state = { authData: null }, action) => {
switch (action.type) {
case "LOGIN":
localStorage.setItem("token", JSON.stringify({ data: action.payload }));
return {
...state,
authData: action.payload
};
case "LOGOUT":
localStorage.removeItem("token");
return {
...state,
authData: null
};
default:
return state;
}
};
export default reducer;
This is simply because you are rendering the Auth component only in the "/" route.
So if you are on a different route, you are skipping the "/" route and the user route will be shown. There are different ways on how to handle it:
Always redirect
<Switch>
<Route exact path="/">
<Auth />
</Route>
{!logIn && <Route path="*"><Redirect to={`/profile/${logIn.data.id}`} /></Route>} // This route will not always be rendered if the user should login and is not on the "/" page
<Route exact path="/socialmedia" component={SocialMedia} />
<Route exact path="/profile/:id" component={SingleUser} />
</Switch>
Redirect manually for every route
const SingleUser = () => {
const history = useHistory();
const dispatch = useDispatch();
const userData = JSON.parse(localStorage.getItem("token"));
if(!userData) histroy.push("/")
...
Early return
function App() {
const logIn = JSON.parse(localStorage.getItem("token"));
if(!login) return <Auth />
return (
<Router>
<Switch>
<Route exact path="/">
<Redirect to={`/profile/${logIn.data.id}`} />
</Route>
<Route exact path="/socialmedia" component={SocialMedia} />
<Route exact path="/profile/:id" component={SingleUser} />
</Switch>
</Router>
);
}
Additionally, userData will always be defined on logout, since you are not accessing a new lcoalState but the current one, before it got removed. It still lives in your varibale.
if (!userData) { // Remove the if
history.push("/");
}
Another quite easy solution for you might be just doing this:
<Route exact path="/" render={() => (logIn ? <SingleUser /> : <Auth />)} />
This way on every rerender it will check if there is a user and if not it will redirect you directly to the Auth Component.
Hi mike from the past !
Your code it's okay, but, when you click logout, the variable on the App.js component is still the same, so, you won't get the results you're looking for, what you have to do, it's to reload the website, so te App.js component will upload the variable and will recognize that the value of variable has changed, so you will go to login/register again!
just change your code for this one, so far everything work as expected
import React from "react";
import { useHistory } from "react-router-dom";
import { useDispatch } from "react-redux";
const SingleUser = () => {
const history = useHistory();
const dispatch = useDispatch();
const logout = () => {
dispatch({ type: "LOGOUT" });
history.push("/");
history.go("/");
};
return <div onClick={logout}>Single User</div>;
};
export default SingleUser;

onAuthStateChanged Firebase Listener on app refresh causing private route issues

I have currently initialized a React App with firebase. Within the application, I have created an open login route and a private Home route using react-router-dom. my app.js looks like so:
App.js:
import React from 'react'
import { BrowserRouter, Switch, Route } from 'react-router-dom'
import Login from './pages/Login'
import Home from './pages/Home'
import PrivateRoute from './utils/PrivateRoute'
const App = () => {
return (
<BrowserRouter>
<Switch>
<PrivateRoute exact path='/' component={Home} />
<Route path='/login' component={Login} />
</Switch>
</BrowserRouter>
)
}
export default App
I am storing the currentUser in context using the onAuthStateChanged firebase event listener like so:
AppContext:
import { useEffect, useState, createContext } from 'react'
import { auth } from '../utils/firebase'
export const AppContext = createContext()
export const AppProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState(null)
useEffect(() => {
auth.onAuthStateChanged(setCurrentUser)
}, [])
return (
<AppContext.Provider value={{ currentUser }}>
{children}
</AppContext.Provider>
)
}
when a user logins in via the login route:
login:
import React, { useState, useCallback, useContext } from 'react'
import { auth } from '../utils/firebase'
import { useHistory, Redirect } from 'react-router-dom'
function Login() {
const [formData, setFormData] = useState({ email: '', password: '' })
const history = useHistory()
const handleChange = ({ target: { name, value } }) => {
setFormData({ ...formData, [name]: value })
}
const handleSubmit = useCallback(
async event => {
event.preventDefault()
await auth
.createUserWithEmailAndPassword(formData.email, formData.password)
.then(user => {
console.log(user)
history.push('/')
})
.catch(err => {
alert(err)
})
},
[history, formData.email, formData.password]
)
return (
<div className='form-container sign-up-container'>
<form className='register-form' onSubmit={handleSubmit}>
<h1>Create Account</h1>
<div className='social-container'>
<div className='social'>
<i className='fab fa-facebook-f'></i>
</div>
<div className='social'>
<i className='fab fa-google-plus-g'></i>
</div>
<div className='social'>
<i className='fab fa-linkedin-in'></i>
</div>
</div>
<span>or use your email for registration</span>
<input
type='email'
placeholder='Email'
name='email'
onChange={handleChange}
/>
<input
type='password'
placeholder='Password'
name='password'
onChange={handleChange}
/>
<button type='submit'>Sign Up</button>
</form>
</div>
)
}
export default Login
the currentUser is successfully stored in context and the user is pushed into the private Home route.
the Private Route looks like so:
import React, { useContext } from 'react'
import { Route, Redirect } from 'react-router-dom'
import { AppContext } from '../context/AppContext'
const PrivateRoute = ({ component: Component, ...rest }) => {
const { currentUser } = useContext(AppContext)
return (
<Route
{...rest}
render={routeProps =>
!!currentUser ? (
<Component {...routeProps} />
) : (
<Redirect to={'/login'} />
)
}
/>
)
}
export default PrivateRoute
The issue I'm having is that when the app refreshes, the currentUser becomes null initially and then currentUser's information loads back up. While the currentUser is null on refresh, the user is kicked from the home route and redirected to the login page. I'm wondering if anyone has any suggestions on how to prevent this from happening.
You initialize your currentUser state variable with a null value:
const [currentUser, setCurrentUser] = useState(null)
This means that currentUser is always initially null when the page is first loaded. The prior user object isn't known for sure until some time later, after it's asynchronously loaded by the Firebase SDK. Your code needs to be ready for this. If you require that a user be signed in before rendering your component, you should wait for the first time onAuthStateChanged triggers with an actual user object.
You can read more about this behavior of the Firebase Auth SDK in this blog.

What is the best way to make private route in reactjs

I made a private route in my app basically all the route in my app is private. So to make Private route I added a state isAuthenticated which is false by default but when user login in it becomes true. So on this isAuthenticated state I implemented a condition in private route component. But the problem with this is when user is logged in and refresh the page. I get redirect to / home page. I don't want that.
I am use token authentication here.
Here is my PrivateRoute.js
import React from "react";
import { connect } from "react-redux";
import { Route, Redirect } from "react-router-dom";
const PrivateRoute = ({ isAuthenticated, component: Component, ...rest }) => (
<Route
{...rest}
render={(props) =>
isAuthenticated ? <Component {...props} /> : <Redirect to="/" />
}
/>
);
function mapStateToProps(state) {
return {
isAuthenticated: state.user.isAuthenticated,
};
}
export default connect(mapStateToProps)(PrivateRoute);
If all your routes when authenticated are private you can also just skip the autentication by route and use following
import PrivateApp from './PrivateApp'
import PublicApp from './PublicApp'
function App() {
// set isAuthenticated dynamically
const isAuthenticated = true
return isAuthenticated ? <PrivateApp /> : <PublicApp />
}
That way you do not need to think for each route if it is authenticated or not and can just define it once and depending on that it will render your private or public app. Within those apps you can then use the router normally and do not need to think who can access which route.
If you validate the token against a server (which you should) then it's an asynchronous operation and isAuthenticated will be falsy initially, causing the redirect on refresh.
You one way around this is an isAuthenticating state, e.g.
import React from "react";
import { connect } from "react-redux";
import { Route, Redirect } from "react-router-dom";
const PrivateRoute = ({ isAuthenticated, isAuthenticating, component: Component, ...rest }) => (
<Route
{...rest}
render={(props) => {
if( isAuthenticating ){ return <Spinner /> }
return isAuthenticated ? <Component {...props} /> : <Redirect to="/" />
}
}
/>
);
function mapStateToProps(state) {
return {
isAuthenticated: state.user.isAuthenticated,
isAuthenticating: state.authentication.inProgress
};
}
export default connect(mapStateToProps)(PrivateRoute);
isAuthenticating should be true by default then set to false when the server response is received and the user.isAuthenticated state is known.

Categories

Resources