Authentication with react context and react router v6 problem - reactjs

I'm trying to build an authentication service using react context with react router v6 routes and I can´t go to the expected route. So, my login in service is working, I'm retrieving the user from the back and setting in localStorage, also have a state from context to store the user data (loggedUser), with only id and type at the moment. Probably is something silly but the loggedUser is not updated in the routes file and I don't understand why, and if it's not the context than it's the routing, with the "Warning: Cannot update a component (BrowserRouter) while rendering a different component (ClientRoute).". Thanks in advance, for any help!
Here is the code that I'm using:
AuthContext.jsx file (don't know if used the useEffect correctly with storing the user too)
import React, { createContext, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import user_api from "../Middles/user_api";
export const AuthContext = createContext({});
//Context criado para autenticação, definir permissão de usuário para determinadas rotas
export const AuthProvider = ({ children }) => {
const navigate = useNavigate();
const [loggedUser, setLoggedUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const cachedUser = localStorage.getItem("user");
if(!cachedUser){
console.log("no user");
} else {
console.log("user logged ", cachedUser);
}
if(loggedUser){
localStorage.setItem("user", JSON.stringify(loggedUser));
setIsLoading(false);
navigate('../home', { replace: true });
}
},[loggedUser]);
const login = async (loginValues) => {
try {
const response = await user_api.loginUser(loginValues);
console.log(response.data);
const { userData, token } = response.data;
const user = {
id: userData.id,
type: userData.data.type
}
setLoggedUser(user);
} catch (error) {
console.log(error);
}
}
const logout = () => {
console.log("logout");
}
return(
<AuthContext.Provider value={ {isAuthenticated: !!loggedUser, loggedUser, isLoading, login, logout} }>
{children}
</AuthContext.Provider>
)
}
Routes file (App_router in index.jsx)
import React, { useContext, useState } from 'react';
import { AuthContext, AuthProvider } from '../Contexts/AuthContext';
import {
BrowserRouter as Router,
Route,
Routes,
useNavigate
} from "react-router-dom";
import HomePage from '../Pages/HomePage';
import Exercises from '../Pages/Exercises';
import UserForm from '../Pages/Forms/UserForm';
import Login from '../Pages/Login';
const App_Router = () => {
const navigate = useNavigate();
const { isAuthenticated, loggedUser, isLoading } = useContext(AuthContext);
const ProRoute = ({ children }) => {
const cachedUser = JSON.parse(localStorage.getItem("user"));
if(cachedUser && cachedUser.type == 'Profissional'){
return children;
}
if(isLoading){
return <section>Carregando...</section>;
}
if(isAuthenticated || loggedUser?.type == "Profissional"){
return children;
}
else {
console.log("Usuário não autorizado.");
navigate('/login', {replace: true});
}
}
const ClientRoute = ({ children }) => {
const cachedUser = JSON.parse(localStorage.getItem("user"));
console.log("cachedUser ", cachedUser);
if(cachedUser && cachedUser.type == 'Profissional'){
return children;
}
if(isLoading){
return <section>Carregando...</section>;
}
if(isAuthenticated || loggedUser?.type == "Cliente"){
return children;
}
else {
console.log("Usuário não autorizado.");
navigate('/login', {replace: true});
}
}
return (
<AuthProvider>
<Routes>
<Route index path='/' element={<Login />} />
<Route exact path='/login' element={<Login/>}/>
<Route exact path='/register' element={<UserForm/>}/>
<Route
path='/home/Profissional'
element={
<ProRoute>
<HomePage/>
</ProRoute>
}/>
<Route
path='/home'
element={
<ClientRoute>
<HomePage/>
</ClientRoute>
}/>
<Route
path='/home/Profissional/exercises'
element={
<ProRoute>
<Exercises/>
</ProRoute>
}/>
</Routes>
</AuthProvider>
)
}
export default App_Router;
Login file
import React, { useContext, useState } from 'react'
import { Link } from 'react-router-dom';
import { AuthContext } from '../../Contexts/AuthContext';
import user_api from '../../Middles/user_api';
import './styles.css'
//Componente de Login usado para autenticação e condicionamento de rotas, autenticação de usuário
const Login = () => {
const { login } = useContext(AuthContext);
const [values, setValues] = useState({
email: '',
password: '',
usertype: ''
});
const handleValues = (e) => {
setValues({...values, [e.target.name]: e.target.value});
}
const handleSubmit = async (e) => {
e.preventDefault();
login(values)
.then(
res => {
setValues({
email: '',
password: '',
usertype: ''
});
})
}
return (
<main id="login-wrapper">
<aside>Chamada de API de treino</aside>
<section id="login-container">
<form onSubmit={handleSubmit}>
<label htmlFor="email">Email</label>
<input type="email" name='email' value={values.email} onChange={handleValues}/>
<label htmlFor="password">Senha</label>
<input type="password" name='password' value={values.password} onChange={handleValues}/>
Esqueceu a senha?
<fieldset>
<legend>Tipo:</legend>
<input type="radio" name='usertype' checked={values.usertype === 'Profissional'} value='Profissional' onChange={handleValues} />
<label htmlFor="usertype">Profisisonal</label>
<input type="radio" name='usertype' checked={values.usertype === 'Cliente'} value='Cliente' onChange={handleValues} />
<label htmlFor="">Aluno</label>
</fieldset>
<button type='submit'>Entrar</button>
</form>
</section>
<aside>
<h4>Ainda não possui conta?</h4>
<Link to="/register">Cadastre-se</Link>
</aside>
</main>
)
}
export default Login;
Here is the full error log that pops when I try to login:
react_devtools_backend.js:4026 Warning: Cannot update a component (`BrowserRouter`) while rendering a different component (`ClientRoute`). To locate the bad setState() call inside `ClientRoute`, follow the stack trace as described in https://reactjs.org/link/setstate-in-render
at ClientRoute (http://localhost:8888/src/Routes/index.jsx:55:5)
at RenderedRoute (http://localhost:8888/node_modules/.vite/deps/react-router-dom.js?v=f7b60e3a:2434:5)
at Routes (http://localhost:8888/node_modules/.vite/deps/react-router-dom.js?v=f7b60e3a:2744:5)
at AuthProvider (http://localhost:8888/src/Contexts/AuthContext.jsx:22:3)
at App_Router (http://localhost:8888/src/Routes/index.jsx:26:20)
Please help, I'm relative new to react and this would be much appreciated. Thanks!

The main issue with the code is that the protected route components are calling navigate as an unintentional side-effect, i.e. outside the useEffect hook. They should either call navigate from the useEffect hook or as a more conventional method just render the Navigate component. A secondary issue that may or may not be contributing to any issues is that both the ProRoute and ClientRoute components are being declared inside another React component; this is generally considered an anti-pattern.
Declare the route protection components on their own and rewrite them to render the Navigate component and consume the AuthContext.
App_Router
import {
BrowserRouter as Router,
Route,
Routes,
Navigate,
Outlet,
} from "react-router-dom";
const ProRoute = ({ children }) => {
const { isAuthenticated, loggedUser, isLoading } = useContext(AuthContext);
if (isLoading) {
return <section>Carregando...</section>;
}
if (isAuthenticated || loggedUser?.type == "Profissional") {
return children || <Outlet />;
} else {
return <Navigate to='/login' replace />;
}
}
const ClientRoute = ({ children }) => {
const { isAuthenticated, loggedUser, isLoading } = useContext(AuthContext);
if (isLoading) {
return <section>Carregando...</section>;
}
if (isAuthenticated || loggedUser?.type == "Cliente") {
return children || <Outlet />;
} else {
return <Navigate to='/login' replace />;
}
}
const App_Router = () => {
return (
<AuthProvider>
<Routes>
<Route path='/' element={<Login />} />
<Route path='/login' element={<Login />} />
<Route path='/register' element={<UserForm />} />
<Route path='/home'>
<Route element={<ClientRoute />}>
<Route index element={<HomePage />} />
</Route>
<Route element={<ProRoute />}>
<Route path='Profissional'>
<Route index element={<HomePage />} />
<Route path='exercises' element={<Exercises />} />
</Route>
</Route>
</Route>
</Routes>
</AuthProvider>
);
}
export default App_Router;

Related

Is there any sense to useContext in React?

Hello i need help with my app.
What sense is to put my User data to useContext if when i refresh page all data disapear?
How to make this context permament?
I have been sitting with this all day, and when i try to get data in HomePage, everything is ok until i refresh page.
The second question is about JWT. It's used only on server side, right? It's verifing the token when I'm making server reqest only?
below is my code
App.js
import Navbar from "./components/Navbar";
import AddNewAnnoucement from "./components/pages/AddNewAnnoucement/AddNewAnnoucement";
import { Route, Routes } from 'react-router-dom';
import Home from "./components/pages/Home";
import Annoucements from "./components/pages/Annoucements/Annoucements";
import Register from "./components/pages/Register/Register";
import Login from "./components/pages/Login/Login";
import { useState, useMemo, useEffect, createContext } from "react";
import { getProfile, tokenIsValid } from './service/userService.js'
export const UserContext = createContext();
function App() {
const [userData, setUserData] = useState({
token: undefined,
user: undefined,
})
useEffect(() => {
const isLoggedIn = async () => {
let token = localStorage.getItem("token")
if (token == null) {
localStorage.setItem("token", "")
token = ""
}
const tokenResponse = tokenIsValid(token);
tokenResponse.then((res) => {
if (res.data) {
const userResponse = getProfile(token);
userResponse.then((res) => {
setUserData({
token: token,
data: res.data
})
})
}
}).catch((err) => {
console.log(err);
}
)
}
isLoggedIn();
}, [])
return <>
<UserContext.Provider value={{ userData, setUserData }}>
<Navbar />
<div className='container'>
<Routes>
<Route path='/' element={<Home />} />
<Route path='/annoucements' element={<Annoucements />} />
<Route path='/annoucements/add' element={<AddNewAnnoucement />} />
<Route path='/register' element={<Register />} />
<Route path='/login' element={<Login />} />
</Routes>
</div>
</UserContext.Provider>
</>
}
export default App;

Using react hooks how can a user navigates to protected routes based on admin access

Using protected.route when a user login how can we make a user navigates to private routes like Dashboard and ViewDetails ? If he is an admin user, then display Dashboard, else ViewDetails screen. Can someone please advise on this. I have added a codesandbox link for reference
Codesandbox link
https://codesandbox.io/s/tender-cerf-kss82?file=/src/components/Login.js
login.js
import { useState } from "react";
import { useHistory } from "react-router-dom";
const loginData = [
{ id: 1, email: "mat#test.com", password: "admin123", access: "admin" },
{ id: 1, email: "duo#test.com", password: "test123", access: "user" }
];
const Login = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const history = useHistory();
const onSubmit = (email, password) => {
if (
email === loginData[0].email &&
password === loginData[0].password &&
loginData[0].access === "admin"
) {
history.push("/");
} else {
history.push("/ViewDetails");
}
};
return (
<div>
Login Page <br></br>
<input
type="text"
name="email"
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="text"
name="password"
onChange={(e) => setPassword(e.target.value)}
/>
<input type="button" value="submit" onClick={onSubmit(email, password)} />
</div>
);
};
export default Login;
protected.route.js
import React from "react";
import { Route, Redirect } from "react-router-dom";
export const ProtectedRoute = ({ component: Component, ...rest }) => {
return (
<Route
{...rest}
render={(props) => {
if (localStorage.getItem("loginEmail")) {
return <Component {...props} />;
} else {
return (
<>
<Redirect
to={{
pathname: "/login",
state: {
from: props.location
}
}}
/>
</>
);
}
}}
/>
);
};
App.js
import React, { useState } from "react";
import "./styles.css";
import { BrowserRouter, Route, Switch } from "react-router-dom";
import Dashboard from "./components/Dashboard.js";
import Login from "./components/Login.js";
import ViewDetails from "./components/ViewDetails.js";
import UserLoginProvider from "./components/UserLoginProvider.js";
import UserProfileProvider from "./components/UserProfileProvider.js";
import ProtectedRoute from "./components/protected.route.js";
import ReactDOM from "react-dom";
//var ReactDOM = require("react-dom");
const App = () => {
return (
<BrowserRouter>
<UserLoginProvider>
<UserProfileProvider>
<>
<Switch>
<ProtectedRoute exact path="/" component={Dashboard} />
<ProtectedRoute path="/ViewDetails" component={ViewDetails} />
<Route path="/Login" component={Login} />
</Switch>
</>
</UserProfileProvider>
</UserLoginProvider>
</BrowserRouter>
);
};
ReactDOM.render(
React.createElement(App, null),
document.getElementById("root")
);
export default App;
Here's an example suggestion.
Update the ProtectedRoute component to take an access role prop and conditionally render a Route or Redirect based on any role stored in localStorage. If the role matches, return the expected route, if it exists and doesn't match then redirect home, otherwise redirect to login.
import { Route, Redirect } from "react-router-dom";
export const ProtectedRoute = ({ role, ...rest }) => {
const currentRole = JSON.parse(localStorage.getItem("role"));
if (currentRole === role) {
return <Route {...rest} />;
} else {
return (
<Redirect
to={{
pathname: currentRole ? "/" : "/login",
state: {
from: rest.location
}
}}
/>
);
}
};
Update Login to handle saving an authenticated user's access role into local storage and redirect back to the path they were originally attempting to access.
import { useHistory, useLocation } from "react-router-dom";
const Login = () => {
const history = useHistory();
const { state } = useLocation();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const onSubmit = (email, password) => {
const user = loginData.find(
(el) => el.email === email && el.password === password
);
if (user) {
localStorage.setItem("role", JSON.stringify(user.access));
history.replace(state.from ?? "/");
} else {
console.error("no user match found!");
}
};
return (
<div>
....
</div>
);
};
Update the protected routes you are rendering in your App.
const App = () => {
return (
<UserLoginProvider>
<UserProfileProvider>
<Switch>
<ProtectedRoute
role="admin"
path="/dashboard"
component={Dashboard}
/>
<ProtectedRoute
role="user"
path="/viewDetails"
component={ViewDetails}
/>
<Route path="/Login" component={Login} />
<Route>
.... home page with nav links, etc...
</Route>
</Switch>
</UserProfileProvider>
</UserLoginProvider>
);
};

React Router // redirect to dashboard when logged in

I have a react app and I would like the user to be redirected to the '/dashboard' when logged in and not the home route '/'. I have accomplished this with the '/login' route, but cannot get it to do the same for the '/' home route.
Any help or insight would be appreciated
occasionally when playing around I get the infinite loop error from React
import React, { useEffect, useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { Auth } from "aws-amplify";
import {
BrowserRouter as Router,
Switch,
Route,
Link,
Redirect,
} from "react-router-dom";
import { Provider } from "react-redux";
import ProtectedRoute from "./Utils/ProtectedRoute";
import PublicRoutes from "./Utils/PublicRoute";
import { AuthState, onAuthUIStateChange } from "#aws-amplify/ui-components";
import store from "./store";
import { userLogIn, userLogOut } from "./Actions/userActions";
import NavBar from "./Components/NavBar";
import Home from "./Screens/Home";
import AmplifySignUp from "./Components/AmplifyLogIn";
import Dashboard from "./Screens/Dashboard";
const Routes = (props) => {
const [authState, setAuthState] = useState();
const [user, setUser] = useState();
const [userName, setUserName] = useState();
const dispatch = useDispatch();
const userState = useSelector((s) => s.user);
useEffect(() => {
return onAuthUIStateChange((nextAuthState, authData) => {
setAuthState(nextAuthState);
setUser(authData);
});
});
useEffect(() => {
if (AuthState.SignedIn && user) {
dispatch(userLogIn(user.attributes.email));
}
}, [user]);
useEffect(() => {
if (authState !== AuthState.SignedIn) {
dispatch(userLogOut());
}
}, [user]);
// check auth
const isAuth = async () => {
try {
const status = await Auth.currentAuthenticatedUser();
return status.username;
} catch (err) {}
};
// Auth.currentAuthenticatedUser()
// .then((user) => {
// console.log(user.username)
// setUserName(prevState => {
// if(prevState !== user.username){
// return user.username
// }else {return prevState}
// }
// )
// })
// .catch((err) => console.log(err));
return (
<Router>
<NavBar />
<Switch>
<Route exact path="/">
{userName ? <Redirect from='/' to="/dashboard" /> : <Home />}
</Route>
<Route path="/login">
{authState === AuthState.SignedIn && user ? (
<Redirect to="/dashboard" />
) : (
<AmplifySignUp />
)}
</Route>
<ProtectedRoute path="/dashboard" user={user}>
<Dashboard />
</ProtectedRoute>
</Switch>
</Router>
);
};
export default Routes;
ProtectedRoutes.js
import React from "react";
import { Route, Redirect } from "react-router-dom";
import { useSelector, useDispatch } from "react-redux";
const ProtectedRoutes = ({user, children, ...rest }) => {
console.log(user && user)
return (
<Route
{...rest}
render={() => {
return user ? (
children
) : (
<Redirect
to='/'
/>
);
}}
/>
);
};
export default ProtectedRoutes;
Issue
On this line...
{userName ? <Redirect from='/' to="/dashboard" /> : <Home />}
You're not updating the username state with the currently authenticated user, hence the dashboard redirect never happens.
Possible Solution
Add a function to get the currently authenticated user
Add a useEffect hook to update the username state with the returned user
Code
const getAuthenticatedUser = async () => {
try {
const user = await Auth.currentAuthenticatedUser();
return {
user,
};
} catch (error) {
return { error };
}
};
useEffect(() => {
const { user, error } = await getAuthenticatedUser();
if (!error) {
setUsername(user.username);
}
}, []);

handle protected route logic

I'm creating a protected route for my react project but context values and redux reducers data are not persistent. So what is the optimal way to set, for example isVerified to true if the user is logged. If isVerified === true go to homepage else redirect to login, isVerified needs to be mutated every change in route or refresh, because context or redux data is not persistent.
Do I need to create a separate backend api for just checking the token coming from the client side? then I will add a useEffect in the main App.tsx, something like:
useEffect(() => {
/*make api call, and pass the token stored in the localStorage. If
verified success then: */
setIsVerified(true)
}, [])
Then I can use the isVerified to my protected route
You can create a Middleware component wrapping both, Protected and Non-protected components routes. And inside of each just check if the user is logged, then render conditionally.
This is usually how I implemented,
Protected:
// AuthRoute.js
import React, { useEffect, useState } from "react";
import { Redirect, Route } from "react-router-dom";
export const AuthRoute = ({ exact = false, path, component }) => {
const [isVerified, setIsVerified] = useState(false);
const checkLoginSession = () => {
// Write your verifying logic here
const loggedUser = window.localStorage.getItem("accessToken") || "";
return loggedUser !== "" ? true : false;
};
useEffect(() => {
(async function () {
const sessionState = await checkLoginSession();
return setIsVerified(sessionState);
})();
}, []);
return isVerified ? (
<Route exact={exact} path={path} component={component} />
) : (
<Redirect to={"/login"} />
);
};
Non-protected:
import React from "react";
import { Redirect, Route } from "react-router-dom";
import { useEffect, useState } from "react";
export const NAuthRoute = ({ exact = false, path, component }) => {
const [isVerified, setIsVerified] = useState(false);
const checkLoginSession = () => {
// Write your verifying logic here
const loggedUser = window.localStorage.getItem("accessToken") || "";
return loggedUser !== "" ? true : false;
};
useEffect(() => {
(async function () {
const sessionState = await checkLoginSession();
return setIsVerified(sessionState);
})();
}, []);
return isVerified ?
<Redirect to={"/"} />
: <Route exact={exact} path={path} component={component} />;
};
I usually use this process and so far it is the most helpful way to make a route protected.
ProtectedRoute.js
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
export const ProtectedRoute = ({component: Component, ...rest}) => {
const isSessionAuth = sessionStorage.getItem('token');
return (
<Route
{...rest}
render={props => {
if(isSessionAuth)
{
return (<Component {...props}/>);
}else{
return <Redirect to={
{
pathname: "/",
state: {
from: props.location
}
}
}/>
}
}
}/>
);
};
In the place where you are defining routes you can use this like this.
import React from "react";
import { BrowserRouter, Route, Switch } from "react-router-dom";
import { ProtectedRoute } from './ProtectedRoute.js';
import SignUp from "./pages/SignUp";
import SignIn from "./pages/SignIn";
import Dashboard from "./pages/Dashboard";
import NotFound from "./pages/NotFound";
const Routes = () => (
<BrowserRouter>
<Switch>
<Route exact path="/" component={SignIn} />
<ProtectedRoute exact path="/dashboard" component={Dashboard} />
<Route path="/signup" component={SignUp} />
<Route path="*" component={NotFound} />
</Switch>
</BrowserRouter>
);
export default Routes;

Login redirect rendering twice

I'm using reach router for my routes. I was able to protect the dashboard and expose the login page and redirect if the user is not logged in but now if I enter a url it will do a quick redirecto to login and then to home instead of the page actually entered.
I noticed because of the useEffect to fetch the user the component renders twice: 1 without user (redirects to login) the other one with user and redirects to home.
Routes file
const AdminRoutes = () => {
return (
<Router>
<MainLayout path="/admin">
<HomePage path="/" />
<CarTransfer path="/cartransfers">
<CarTranserList path="/list" />
<CarTransferCreate path="/new" />
</CarTransfer>
<User path="/users">
<UserList path="/list" />
<UserCreate path="/new" />
</User>
</MainLayout>
<LoginPage path="/login" />
</Router>
);
};
Layout file
import { useState, useEffect } from "react";
import { Redirect, useNavigate } from "#reach/router";
import { Layout } from "antd";
import SiderMenu from "./SiderMenu";
import LayoutBanner from "./LayoutBanner";
import { useSelector, useDispatch } from "react-redux";
import {
userSelector,
fetchUserBytoken,
clearState,
} from "../../features/authSlice";
const { Content } = Layout;
const MainLayout = ({ children }) => {
const user = useSelector(userSelector);
const [collapsed, setCollapsed] = useState(false);
const navigate = useNavigate();
const dispatch = useDispatch();
const { isFetching, isError } = useSelector(userSelector);
useEffect(() => {
dispatch(
fetchUserBytoken({
token: localStorage.getItem("token"),
id: localStorage.getItem("id"),
})
);
}, []);
useEffect(() => {
if (isError) {
dispatch(clearState());
navigate("/login");
}
}, [isError]);
const handleOnCollapse = () => {
setCollapsed((prevState) => !prevState);
};
if (isFetching) {
return <div>Loading</div>;
}
if (user.id === "") {
return <Redirect noThrow to="/login" />;
}
return (
<Layout>
<SiderMenu collapsed={collapsed} handleOnCollapse={handleOnCollapse} />
<Layout>
<LayoutBanner
collapsed={collapsed}
handleOnCollapse={handleOnCollapse}
/>
<Content>{children}</Content>
</Layout>
</Layout>
);
};
export default MainLayout;
The second question would be how to get to the same page you were before the login redirect after login.
Thanks

Resources