When I try the below code I get redirected to login page as if I'm not authenticated. Once I login I can't view the about page as it directs me to Welcome page because the logic in login page (if isAuthenticated navigates to Welcome page). If I remove the logic in login page I get stuck in login page only. Why I can't view about page?
PrivateOutlet.js ;
import React from 'react';
import { Outlet, Navigate } from 'react-router-dom';
const PrivateOutlet = ({ isAuthenticated }) => {
if (isAuthenticated ) {
return <Outlet />
} else {
return <Navigate to='login' /> //Go to login
}
};
export default PrivateOutlet;
updated PrivateOutlet.js ;
import React from 'react';
import { connect } from 'react-redux';
import { Outlet, Navigate } from 'react-router-dom';
const PrivateOutlet = ({ isAuthenticated }) => {
return isAuthenticated ? <Outlet /> : <Navigate to='/login' replace />;
};
const mapStateToProps = state => ({
isAuthenticated: state.auth.isAuthenticated
});
export default connect(mapStateToProps)(PrivateOutlet);
App.js
function App() {
return (
<Provider store={store}>
<Router>
<Layout>
<Routes>
<Route path='/' element={<WelcomePage/>} />
<Route path='/home' element={<Home/>} />
<Route element={<PrivateOutlet/>}>
<Route path='/about' element={<About/>} />
</Route>
<Route path='/contact' element={<Contact/>} />
<Route path='/login' element={<Login/>} />
<Route path='/signup' element={<Signup/>} />
<Route path='/reset-password' element={<ResetPassword/>} />
<Route path='/password/reset/confirm/:uid/:token' element={<ResetPasswordConfirm/>} />
<Route path='/activate/:uid/:token' element={<Activate/>} />
<Route path='*' element={<NotFound/>} />
</Routes>
</Layout>
</Router>
</Provider>
);
}
export default App;
login.js
import React, { useState } from 'react';
import { Link, Navigate } from 'react-router-dom';
import { connect } from 'react-redux';
import { Button } from '#mui/material';
import { login } from '../actions/auth';
import './Login.css';
import { Helmet } from 'react-helmet';
function Login({ login, isAuthenticated }) {
const [formData, setFormData] = useState({
email: '',
password: ''
});
const { email, password } = formData;
const onChange = e => setFormData({ ...formData, [e.target.name]: e.target.value});
const onSubmit = e => {
e.preventDefault();
login (email, password)
};
if (isAuthenticated) {
return <Navigate to='/' />
}
return (
<div className='login'>
<Helmet>
<title>Prosperity - Login</title>
<meta
name='description'
content='login page'
/>
</Helmet>
<h1 className='login__title'>Login</h1>
<p className='login__lead'>Login into your Account</p>
<form className='login__form' onSubmit={e => onSubmit(e)}>
<div className='login__form__group'>
<input
className='login__form__input'
type='email'
placeholder='Email *'
name='email'
value={email}
onChange={e => onChange(e)}
required
/>
</div>
<div className='login__form__group'>
<input
className='login__form__input'
type='password'
placeholder='Password *'
name='password'
value={password}
onChange={e => onChange(e)}
minLength='8'
required
/>
</div>
<Button className='login__button__main' type='submit'>Login</Button>
</form>
<p className='link__to__Signup'>
Do not have an account? <Link to='/signup' className='login__link'>Register</Link>
</p>
<p className='link__to__resetPassword'>
Forgot Password? <Link to='/reset-password' className='reset__password__link'>Reset Password</Link>
</p>
</div>
)
};
const mapStateToProps = state => ({
isAuthenticated: state.auth.isAuthenticated
});
export default connect (mapStateToProps, { login }) (Login);
actions/Auth.js ;
import axios from 'axios';
import { setAlert } from './alert';
import {
LOGIN_SUCCESS,
LOGIN_FAIL,
SIGNUP_SUCCESS,
SIGNUP_FAIL,
ACTIVATION_SUCCESS,
ACTIVATION_FAIL,
USER_LOADED_SUCCESS,
USER_LOADED_FAIL,
AUTHENTICATED_SUCCESS,
AUTHENTICATED_FAIL,
PASSWORD_RESET_SUCCESS,
PASSWORD_RESET_FAIL,
PASSWORD_RESET_CONFIRM_SUCCESS,
PASSWORD_RESET_CONFIRM_FAIL,
LOGOUT
} from './types';
export const checkAuthenticated = () => async dispatch => {
if (localStorage.getItem('access')) {
const config = {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
};
const body = JSON.stringify({ token: localStorage.getItem('access') });
try {
const res = await axios.post(`${process.env.REACT_APP_API_URL}/auth/jwt/verify/`, body, config)
if (res.data.code !== 'token_not_valid') {
dispatch({
type: AUTHENTICATED_SUCCESS
});
} else {
dispatch({
type: AUTHENTICATED_FAIL
});
}
} catch (err) {
dispatch({
type: AUTHENTICATED_FAIL
});
}
} else {
dispatch({
type: AUTHENTICATED_FAIL
});
}
};
export const load_user = () => async dispatch => {
if (localStorage.getItem('access')) {
const config = {
headers: {
'Content-Type': 'application/json',
'Authorization': `JWT ${localStorage.getItem('access')}`,
'Accept': 'application/json'
}
};
try {
const res = await axios.get(`${process.env.REACT_APP_API_URL}/auth/users/me/`, config);
dispatch({
type: USER_LOADED_SUCCESS,
payload: res.data
});
}catch (err) {
dispatch({
type: USER_LOADED_FAIL
});
}
} else {
dispatch({
type: USER_LOADED_FAIL
});
}
};
export const login = (email, password) => async dispatch => {
const config = {
headers: {
'Content-Type': 'application/json'
}
};
const body = JSON.stringify({ email, password });
try {
const res = await axios.post(`${process.env.REACT_APP_API_URL}/auth/jwt/create/`, body, config);
dispatch({
type: LOGIN_SUCCESS,
payload: res.data
});
dispatch(setAlert('Authenticated successfully', 'success'));
dispatch(load_user());
}catch (err) {
dispatch({
type: LOGIN_FAIL
});
dispatch(setAlert('Error Authenticating', 'error'));
}
};
export const signup = (name, email, password, re_password) => async dispatch => {
const config = {
headers: {
'Content-Type': 'application/json'
}
};
const body = JSON.stringify({ name, email, password, re_password });
try {
const res = await axios.post(`${process.env.REACT_APP_API_URL}/auth/users/`, body, config);
dispatch({
type: SIGNUP_SUCCESS,
payload: res.data
});
dispatch(setAlert('Check Your Email to Activate Your Account.', 'warning'));
} catch (err) {
dispatch({
type: SIGNUP_FAIL
})
}
};
export const verify = (uid, token) => async dispatch => {
const config = {
headers: {
'Content-Type': 'application/json'
}
};
const body = JSON.stringify({ uid, token });
try {
await axios.post(`${process.env.REACT_APP_API_URL}/auth/users/activation/`, body, config);
dispatch({
type: ACTIVATION_SUCCESS,
});
dispatch(setAlert('Account Activated Successfully.', 'success'));
} catch (err) {
dispatch({
type: ACTIVATION_FAIL
})
}
};
//Reset Password
export const reset_password = (email) => async dispatch => {
const config = {
headers: {
'Content-Type': 'application/json'
}
};
const body = JSON.stringify({ email });
try {
await axios.post (`${process.env.REACT_APP_API_URL}/auth/users/reset_password/`, body, config);
dispatch({
type: PASSWORD_RESET_SUCCESS
});
dispatch(setAlert('Check Your Email to Rest Password.', 'warning'));
} catch (err) {
dispatch({
type: PASSWORD_RESET_FAIL
});
}
};
// Reset Password Confirm
export const reset_password_confirm = (uid, token, new_password, re_new_password) => async dispatch => {
const config = {
headers: {
'Content-Type': 'application/json'
}
};
const body = JSON.stringify({ uid, token, new_password, re_new_password });
try {
await axios.post (`${process.env.REACT_APP_API_URL}/auth/users/reset_password_confirm/`, body, config);
dispatch(setAlert('Password Rest Successful.', 'success'));
dispatch({
type: PASSWORD_RESET_CONFIRM_SUCCESS
});
} catch (err) {
dispatch({
type: PASSWORD_RESET_CONFIRM_FAIL
});
}
};
//Logout
export const logout = () => dispatch => {
dispatch(setAlert('Logout successful.', 'success'));
dispatch({
type: LOGOUT
});
};
reducers/Auth.js ;
import {
LOGIN_SUCCESS,
LOGIN_FAIL,
SIGNUP_SUCCESS,
SIGNUP_FAIL,
ACTIVATION_SUCCESS,
ACTIVATION_FAIL,
USER_LOADED_SUCCESS,
USER_LOADED_FAIL,
AUTHENTICATED_SUCCESS,
AUTHENTICATED_FAIL,
PASSWORD_RESET_SUCCESS,
PASSWORD_RESET_FAIL,
PASSWORD_RESET_CONFIRM_SUCCESS,
PASSWORD_RESET_CONFIRM_FAIL,
LOGOUT
} from '../actions/types';
const initialState = {
access: localStorage.getItem('access'),
refresh: localStorage.getItem('refresh'),
isAuthenticated: null,
user: null,
};
export default function (state = initialState, action) {
const { type, payload } = action;
switch(type) {
case AUTHENTICATED_SUCCESS:
return {
...state,
isAuthenticated: true
}
case LOGIN_SUCCESS:
localStorage.setItem('access', payload.access);
localStorage.setItem('refresh', payload.refresh);
return {
...state,
isAuthenticated: true,
access: payload.access,
refresh: payload.refresh,
}
case USER_LOADED_SUCCESS:
return {
...state,
user: payload
}
case SIGNUP_SUCCESS:
return {
...state,
isAuthenticated: false,
}
case AUTHENTICATED_FAIL:
return {
...state,
isAuthenticated: false
}
case USER_LOADED_FAIL:
return {
...state,
user: null
}
case LOGIN_FAIL:
case SIGNUP_FAIL:
case LOGOUT:
localStorage.removeItem('access');
localStorage.removeItem('refresh');
return {
...state,
access: null,
refresh: null,
isAuthenticated: false,
user: null,
}
case PASSWORD_RESET_SUCCESS:
case PASSWORD_RESET_FAIL:
case ACTIVATION_SUCCESS:
case ACTIVATION_FAIL:
case PASSWORD_RESET_CONFIRM_SUCCESS:
case PASSWORD_RESET_CONFIRM_FAIL:
return {
...state
}
default:
return state
}
};
isAuthenticated isn't passed as a prop to PrivateOutlet.
<Route element={<PrivateOutlet />}> // <-- no isAuthenticated prop
<Route path='/about' element={<About />} />
</Route>
isAuthenticated is stored in redux state and the initial value is null and not the true|false after a successful or failed authentication attempt.
const initialState = {
access: localStorage.getItem('access'),
refresh: localStorage.getItem('refresh'),
isAuthenticated: null,
user: null,
};
You can explicitly check if the isAuthenticated state is null and conditionally return null or a loading indicator, etc... while the authentication status is being resolved. Once the authentication status resolves to a non-null value then either the routed component or redirect can be rendered.
import React from 'react';
import { connect } from 'react-redux';
import { Outlet, Navigate } from 'react-router-dom';
const PrivateOutlet = ({ isAuthenticated }) => {
if (isAuthenticated === null) {
return null;
}
return isAuthenticated ? <Outlet /> : <Navigate to='/login' replace />;
};
const mapStateToProps = state => ({
isAuthenticated: state => state.auth.isAuthenticated,
});
export default connect(mapStateToProps)(PrivateOutlet);
or
import React from 'react';
import { useSelector } from 'react-redux';
import { Outlet, Navigate } from 'react-router-dom';
const PrivateOutlet = () => {
const isAuthenticated = useSelector(state => state.auth.isAuthenticated);
if (isAuthenticated === null) {
return null;
}
return isAuthenticated ? <Outlet /> : <Navigate to='/login' replace />;
};
export default PrivateOutlet;
Update
If you want to redirect a user back to the page they were originally trying to access the PrivateOutlet component should grab the current location and pass this in route state to the login page.
import { Outlet, Navigate, useLocation } from 'react-router-dom';
const PrivateOutlet = () => {
const location = useLocation();
const isAuthenticated = useSelector(state => state.auth.isAuthenticated);
if (isAuthenticated === null) {
return null;
}
return isAuthenticated
? <Outlet />
: <Navigate to='/login' state={{ from: location }} replace />;
};
Then the Login component grabs this value from route state to imperatively navigate back to the original route.
const navigate = useNavigate();
const { state } = useLocation();
const { from } = state || {};
...
navigate(from.pathname || "/home", { state: from.state, replace: true });
Example
import React, { useState } from 'react';
import { Link, Navigate, useLocation } from 'react-router-dom';
...
function Login({ login, isAuthenticated }) {
const { state } = useLocation();
const { from } = state || {};
const [formData, setFormData] = useState({
email: '',
password: ''
});
const { email, password } = formData;
const onChange = e => setFormData({
...formData,
[e.target.name]: e.target.value
});
const onSubmit = e => {
e.preventDefault();
login(email, password);
};
if (isAuthenticated) {
return (
<Navigate
to={from.pathname || "/home"}
replace
state={from.state}
/>
);
}
return (
...
);
};
const mapStateToProps = state => ({
isAuthenticated: state.auth.isAuthenticated
});
Update 2
I get on the console; Login.js:30 Uncaught TypeError: Cannot read properties of undefined (reading 'pathname')
I think what is occurring here is that the login action updates your redux store, which should trigger the component to rerender, and I suspect it's this rerender that looses the route state. Route state is pretty transient and only exists during the transition and render cycle when it's received. You could probably use a React ref to cache a copy of the route state to use later.
Example:
import React, { useRef, useState } from 'react';
import { Link, Navigate, useLocation } from 'react-router-dom';
...
function Login({ login, isAuthenticated }) {
const { state } = useLocation();
const { from } = state || {};
const fromRef = useRef(from);
const [formData, setFormData] = useState({
email: '',
password: ''
});
const { email, password } = formData;
const onChange = e => setFormData({
...formData,
[e.target.name]: e.target.value
});
const onSubmit = e => {
e.preventDefault();
login(email, password);
};
if (isAuthenticated) {
return (
<Navigate
to={fromRef.current.pathname || "/home"}
replace
state={fromRef.current.state}
/>
);
}
return (
...
);
};
It may be more practical to pass an "onLoginSuccess" handler to the login action, and issue an imperative navigation from the asynchronous action.
Example:
import React, { useState } from 'react';
import { Link, useNavigate, useLocation } from 'react-router-dom';
...
function Login({ login, isAuthenticated }) {
const navigate = useNavigate();
const { state } = useLocation();
const { from } = state || {};
const [formData, setFormData] = useState({
email: '',
password: ''
});
const { email, password } = formData;
const onChange = e => setFormData({
...formData,
[e.target.name]: e.target.value
});
const onSubmit = e => {
e.preventDefault();
const onSuccess = () => {
navigate(
from.pathname || "/home",
{
replace: true,
state: from.state
}
);
};
login(email, password, onSuccess);
};
return (
...
);
};
...
export const login = (email, password, onSuccess) => async dispatch => {
const config = {
headers: {
'Content-Type': 'application/json'
}
};
const body = JSON.stringify({ email, password });
try {
const res = await axios.post(
`${process.env.REACT_APP_API_URL}/auth/jwt/create/`,
body,
config
);
dispatch({ type: LOGIN_SUCCESS, payload: res.data });
dispatch(setAlert('Authenticated successfully', 'success'));
dispatch(load_user());
if (onSuccess) {
onSuccess();
}
} catch (err) {
dispatch({ type: LOGIN_FAIL });
dispatch(setAlert('Error Authenticating', 'error'));
}
};
Related
On my website switching between pages is completely fine and works (it doesnt refresh or load due to redux) but the moment the page is refreshed or i manually enter a link to access, it logs me out. Also it just started happening now, yesterday when i was working on some other things in code, it never logged me out when I manually with links/urls navigated thru website or refreshing but now for some reason it doesnt work and I'm 99% sure I havent touched any auth part of the code...
This is my code:
authApiSlice:
import { apiSlice } from "../../app/api/apiSlice";
import { logOut, setCredentials } from "./authSlice";
export const authApiSlice = apiSlice.injectEndpoints({
endpoints: (builder) => ({
login: builder.mutation({
query: (credentials) => ({
url: "/auth",
method: "POST",
body: { ...credentials },
}),
}),
sendLogout: builder.mutation({
query: () => ({
url: "/auth/logout",
method: "POST",
}),
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
try {
const { data } = await queryFulfilled;
console.log(data);
dispatch(logOut());
setTimeout(() => {
dispatch(apiSlice.util.resetApiState());
}, 1000);
} catch (err) {
console.log(err);
}
},
}),
refresh: builder.mutation({
query: () => ({
url: "/auth/refresh",
method: "GET",
}),
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
try {
const { data } = await queryFulfilled;
console.log(data);
const { accessToken } = data;
dispatch(setCredentials({ accessToken }));
} catch (err) {
console.log(err);
}
},
}),
}),
});
export const { useLoginMutation, useSendLogoutMutation, useRefreshMutation } =
authApiSlice;
authSlice:
import { createSlice } from "#reduxjs/toolkit";
const authSlice = createSlice({
name: "auth",
initialState: { token: null },
reducers: {
setCredentials: (state, action) => {
const { accessToken } = action.payload;
state.token = accessToken;
},
logOut: (state, action) => {
state.token = null;
},
},
});
export const { setCredentials, logOut } = authSlice.actions;
export default authSlice.reducer;
export const selectCurrentToken = (state) => state.auth.token;
import { Outlet, Link } from "react-router-dom";
import { useEffect, useRef, useState } from "react";
import { useRefreshMutation } from "./authApiSlice";
import usePersist from "../../hooks/usePersist";
import { useSelector } from "react-redux";
import { selectCurrentToken } from "./authSlice";
const PersistLogin = () => {
const [persist] = usePersist();
const token = useSelector(selectCurrentToken);
const effectRan = useRef(false);
const [trueSuccess, setTrueSuccess] = useState(false);
const [refresh, { isUninitialized, isLoading, isSuccess, isError, error }] =
useRefreshMutation();
useEffect(() => {
if (effectRan.current === true || process.env.NODE_ENV !== "development") {
// React 18 Strict Mode
const verifyRefreshToken = async () => {
console.log("verifying refresh token");
try {
//const response =
await refresh();
//const { accessToken } = response.data
setTrueSuccess(true);
} catch (err) {
console.error(err);
}
};
if (!token && persist) verifyRefreshToken();
}
return () => (effectRan.current = true);
// eslint-disable-next-line
}, []);
let content;
if (!persist) {
// persist: no
console.log("no persist");
content = <Outlet />;
} else if (isLoading) {
//persist: yes, token: no
console.log("loading");
} else if (isError) {
//persist: yes, token: no
console.log("error");
content = (
<p className="errmsg">
{`${error?.data?.message} - `}
<Link to="/login">Please login again</Link>.
</p>
);
} else if (isSuccess && trueSuccess) {
//persist: yes, token: yes
console.log("success");
content = <Outlet />;
} else if (token && isUninitialized) {
//persist: yes, token: yes
console.log("token and uninit");
console.log(isUninitialized);
content = <Outlet />;
}
return content;
};
export default PersistLogin;
RequireAuth
import { useLocation, Navigate, Outlet } from "react-router-dom";
import useAuth from "../../hooks/useAuth";
const RequireAuth = ({ allowedRoles }) => {
const location = useLocation();
const { roles } = useAuth();
const content = roles.some((role) => allowedRoles.includes(role)) ? (
<Outlet />
) : (
<Navigate to="/prijava" state={{ from: location }} replace />
);
return content;
};
export default RequireAuth;
It just stopped worked for some reason, it shouldnt log me out when I refresh.
I have done jwt authorization and registration. But when checking whether the user is authorized, he issues 401 Unauthorized sent a request to receive a refresh token via axios
Here is the authorization verification action:
export const checkAuth = () => {
try {
return async dispatch => {
const response = await axios.get(`http://localhost:5000/api/refresh`, {withCredentials: true})
//console.log(response)
dispatch({
type: SET_USER,
payload: response.data
})
}
} catch (e) {
console.log(e.response?.data?.message);
}
}
here is the reducer:
import {LOGOUT, SET_USER} from "../types";
const initialState = {
currentUser: {},
isAuth: false
}
export const authReducer = (state = initialState, action) => {
switch (action.type) {
case SET_USER:
localStorage.setItem('token', action.payload.accessToken);
return {
...state,
currentUser: action.payload,
isAuth: true
}
case LOGOUT:
localStorage.removeItem('token')
return {
...state,
currentUser: {},
isAuth: false
}
default:
return state
}
}
export const logout = () => ({type: LOGOUT})
Here is the app component:
import './App.css';
import {Route, Routes} from "react-router-dom";
import LoginForm from "./component/LoginForm";
import {useDispatch, useSelector} from "react-redux";
import RegisterForm from "./component/RegisterForm";
import {useEffect} from "react";
import {checkAuth} from "./redux/action";
function App() {
const users = useSelector(state => {
const {authReducer} = state
return authReducer
})
const dispatch = useDispatch()
//console.log(users)
useEffect(() => {
checkAuth() // here I did dispatch()
}, [])
if (!users.isAuth) {
return (<div>
<Routes>
<Route path="/registration" element={<RegisterForm/>}/>
<Route path="/login" element={<LoginForm/>}/>
</Routes>
</div>)
}
console.log(users.currentUser)
return (
<div className="App">
{users.isAuth ? users.currentUser.rows.map(data => <div key={data.id}>{data.email}</div>) : 'log in'}
</div>
);
}
export default App;
Here are the axios settings, but I tried using fetch() and the result is the same (401)
import axios from "axios";
export const API_URL = `http://localhost:5000/api`
const $api = axios.create({
withCredentials: true,
baseURL: API_URL
})
$api.interceptors.request.use((config) => {
config.headers.Authorization = `Bearer ${localStorage.getItem('token')}`
return config;
})
$api.interceptors.response.use((config) => {
return config;
},async (error) => {
const originalRequest = error.config;
if (error.response.status == 401 && error.config && !error.config._isRetry) {
originalRequest._isRetry = true;
try {
const response = await axios.get(`${API_URL}/refresh`, {withCredentials: true})
localStorage.setItem('token', response.data.accessToken);
return $api.request(originalRequest);
} catch (e) {
console.log('NOT AUTHORIZED')
}
}
throw error;
})
export default $api;
I installed cars on the server and call it like this
app.use(cors()) // Use this after the variable declaration / I tried such settings, it didn't help {credentials: true, origin: 'http://localhost:3000'}
For some reason, a 401 error pops up, although everything works in postman!
Why don't I know anymore!???
I am trying to check if the user is logged in or not in Register.js component but I am getting undefined values of auth state in it. Please look into it if you can find where I am doing wrong. I have tried doing a lot of changes still I was not able to get it right.
Thank you for all of your help.
Ignore the context files
App.js
import { useEffect } from "react";
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
import "./App.css";
import { Navbar } from "./components/layout/Navbar";
import { Home } from "./components/pages/Home";
import { MyList } from "./components/favorites/MyList";
import Register from "./components/auth/Register";
import Login from "./components/auth/Login";
import { Provider } from "react-redux";
import store from "./store";
import { loadUser } from "./actions/auth";
import { setAuthToken } from "./utils/setAuthToken";
if (localStorage.token) {
setAuthToken(localStorage.token);
}
function App() {
useEffect(() => {
store.dispatch(loadUser());
}, []);
return (
<Provider store={store}>
<>
<div>
<Navbar />
<Routes>
<Route path='/' element={<Home />} />
<Route path='/mylist' element={<MyList />} />
<Route path='/signin' element={<Login />} />
<Route path='/signup' element={<Register />} />
</Routes>
</div>
</>
</Provider>
);
}
export default App;
Register.js
import React, { useContext, useEffect, useState } from "react";
import { Link, Navigate, useNavigate } from "react-router-dom";
import { connect } from "react-redux";
import { register } from "../../actions/auth";
import axios from "axios";
const Register = ({ register, isAuthenticated, history, location, auth }) => {
const navigate = useNavigate();
const [user, setUser] = useState({
name: "",
email: "",
password: "",
password2: "",
});
const { name, email, password, password2 } = user;
const onFormDataChange = (e) => {
setUser({
...user,
[e.target.name]: e.target.value,
});
};
const onFormSubmit = async (e) => {
e.preventDefault();
register({ name, email, password });
};
console.log(auth);
console.log(isAuthenticated);
return (
<div>
<h1>Sign Up</h1>
<form action='' onSubmit={onFormSubmit}>
<div className='form-group'>
<label htmlFor='name'>Name</label>
<input
type='text'
name='name'
value={name}
onChange={onFormDataChange}
/>
</div>
<div className='form-group'>
<label htmlFor='email'>Email</label>
<input
type='email'
name='email'
value={email}
onChange={onFormDataChange}
/>
</div>
<div className='form-group'>
<label htmlFor='password'>Password</label>
<input
type='password'
name='password'
value={password}
onChange={onFormDataChange}
/>
</div>
<div className='form-group'>
<label htmlFor='password2'>Password</label>
<input
type='password'
name='password2'
value={password2}
onChange={onFormDataChange}
/>
</div>
<input type='submit' value='Signup' />
<p>
Already have an account? <Link to='/signin'>Sign In</Link>
</p>
</form>
</div>
);
};
const mapStateToProps = (state) => {
const { isAuthenticated } = state.auth.isAuthenticated;
const { auth } = state.auth;
return {
isAuthenticated,
};
};
export default connect(mapStateToProps, { register })(Register);
index.js
import { combineReducers } from "redux";
import auth from "./auth";
export default combineReducers({
auth,
});
/auth/types.js
export const REGISTER_SUCCESS = "REGISTER_SUCCESS";
export const REGISTER_FAIL = "REGISTER_FAIL";
export const USER_LOADED = "USER_LOADED";
export const AUTH_ERROR = "AUTH_ERROR";
export const LOGIN_SUCCESS = "LOGIN_SUCCESS";
export const LOGIN_FAIL = "LOGIN_FAIL";
export const LOGOUT = "LOGOUT";
export const CLEAR_ERRORS = "CLEAR_ERRORS";
actions/auth.js
import axios from "axios";
import {
REGISTER_SUCCESS,
REGISTER_FAIL,
USER_LOADED,
AUTH_ERROR,
LOGIN_SUCCESS,
LOGIN_FAIL,
} from "./types";
import { setAuthToken } from "../utils/setAuthToken";
export const loadUser = () => async (dispatch) => {
if (localStorage.token) {
setAuthToken(localStorage.token);
}
try {
const res = await axios.get("/api/auth");
dispatch({
type: USER_LOADED,
payload: res.data,
});
} catch (error) {
dispatch({
type: AUTH_ERROR,
});
}
};
export const register =
({ name, email, password }) =>
async (dispatch) => {
const config = {
headers: {
"Content-Type": "application/json",
},
};
const body = JSON.stringify({ name, email, password });
try {
const res = await axios.post("/api/users", body, config);
dispatch({
type: REGISTER_SUCCESS,
payload: res.data,
});
dispatch(loadUser());
} catch (error) {
dispatch({
type: REGISTER_FAIL,
});
}
};
export const login = (email, password) => async (dispatch) => {
const config = {
headers: {
"Content-Type": "application/json",
},
};
const body = JSON.stringify({ email, password });
try {
const res = await axios.post("/api/auth", body, config);
dispatch({
type: LOGIN_SUCCESS,
payload: res.data,
});
dispatch(loadUser());
} catch (error) {
dispatch({
type: LOGIN_FAIL,
});
}
};
reducers/auth.js
/* eslint-disable default-case */
/* eslint-disable import/no-anonymous-default-export */
import {
REGISTER_SUCCESS,
REGISTER_FAIL,
USER_LOADED,
AUTH_ERROR,
LOGIN_SUCCESS,
LOGIN_FAIL,
} from "../actions/types";
const initalState = {
token: localStorage.getItem("token"),
isAuthenticated: false,
loading: true,
user: null,
};
export default function (state = initalState, action) {
const { type, payload } = action;
switch (type) {
case USER_LOADED:
return {
...state,
user: payload,
isAuthenticated: true,
loading: false,
};
case REGISTER_SUCCESS:
case LOGIN_SUCCESS:
localStorage.setItem("token", payload.token);
return {
...state,
...payload,
isAuthenticated: true,
loading: false,
};
case REGISTER_FAIL:
case AUTH_ERROR:
case LOGIN_FAIL:
localStorage.removeItem("token");
return {
...state,
token: null,
isAuthenticated: false,
loading: true,
};
default:
return state;
}
}
store.js
import { createStore, applyMiddleware } from "redux";
import { composeWithDevTools } from "redux-devtools-extension";
import thunk from "redux-thunk";
import rootReducer from "./reducers";
const intialState = {};
const middleware = [thunk];
const store = createStore(
rootReducer,
intialState,
composeWithDevTools(applyMiddleware(...middleware))
);
export default store;
I might be wrong, but in you Register.js, in mapStateToProps, you doing following thing:
const { isAuthenticated } = state.auth.isAuthenticated;
const { auth } = state.auth;
It seems like it's not correct, unless your state has following structure:
{
auth: {
isAuthenticated: { isAuthenticated: *value here* },
auth: *value here*
}
}
I think, here might be mistake, and you should destructure values following way:
const { isAuthenticated } = state.auth;
const { auth } = state;
I have created two route components, one for authenticated users and the other for guest. each of the routes checks on the redux state for the authenticated property to determine if a user is logged in or not in order to make the appropriate redirection. Now, the issues is that the checking and redirection should only happen once when a user changes the route but instead each of the route components re-renders when the authenticated property changes on the redux state. This is affecting my login flow because the guest route components automatically handles the redirection instead of the login component.
Below is code:
App.js component:
import './App.css'
import { lazy, useEffect } from 'react'
import { Route, BrowserRouter as Router, Routes } from 'react-router-dom'
import Layout from './components/Layout'
import AuthRoute from './components/AuthRoute'
import Error from './components/ErrorPage'
import configData from './config/configData.json'
import axios from 'axios'
// import { useEffect } from 'react'
// Import Css
import './assets/css/materialdesignicons.min.css'
import './Apps.scss'
import './assets/css/colors/default.css'
import SetupBusiness from './components/Business/SetupBusiness'
import Preview from './components/Business/Preview'
import About from './components/Business/About'
import Media from './components/Business/Media'
import { logoutAction } from './state/action-creators/AuthAction'
import { connect } from 'react-redux'
import PublicRoute from './components/PublicRoute'
const Register = lazy(() => import('./controller/Register'))
const Login = lazy(() => import('./controller/Login'))
function App(props) {
useEffect(() => {
async function checkUserAuth() {
const token = localStorage.getItem('token')
if (token) {
axios
.get(`${configData.SERVER_URL}/user/me`, {
headers: {
Authorization: `Bearer ${token}`,
},
})
.then(() => {})
.catch((error) => {
console.log('nbvhg')
props.logout()
})
}
}
checkUserAuth()
setInterval(checkUserAuth, 15000)
}, [props])
return (
<div className="App">
<Router>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<h1>Homepage</h1>} />
<Route path="/" element={<PublicRoute />}>
<Route path="/register" element={<Register />} />
<Route path="/login" element={<Login />} />
</Route>
<Route path="/business/setup" element={<AuthRoute />}>
<Route element={<SetupBusiness />}>
<Route path="about" element={<About />} />
<Route path="media" element={<Media />} />
<Route path="preview" element={<Preview />} />
</Route>
</Route>
</Route>
<Route path="*" element={<Error />} />
</Routes>
</Router>
</div>
)
}
const mapDispatchToProps = (dispatch) => {
return {
logout: () => {
return dispatch(logoutAction())
},
}
}
export default connect(null, mapDispatchToProps)(App)
PublicRoute component:
import { connect } from 'react-redux'
import { Navigate, Outlet } from 'react-router-dom'
import React from 'react'
class PublicRoute extends React.Component {
render() {
const { authenticated } = this.props
if (authenticated) {
return <Navigate to="/" />
}
return <Outlet />
}
}
const mapStateToProps = (state) => ({
authenticated: state.auth.authenticated,
})
export default connect(mapStateToProps, null)(PublicRoute)
Login component:
// React Basic and Bootstrap
import React from 'react'
import { connect } from 'react-redux'
import Cookies from 'js-cookie'
import { loginAction } from '../state/action-creators/AuthAction'
import View from '../components/Login'
class Login extends React.Component {
constructor(props) {
super(props)
this.state = {
values: {
email_address: '',
password: '',
},
errors: {},
isSubmitting: false,
redirect: null,
}
this.handleChange = this.handleChange.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
}
handleChange(event) {
let values = Object.assign({}, this.state.values)
let target = event.target
let name = target.name
let value = target.value
if (typeof values[name] !== 'undefined') {
if (name === 'checked') {
value = target.checked
}
values[name] = value
}
this.setState({ values })
}
validation() {
const errors = {}
let values = this.state.values
//email validation
if (!values.email_address.trim()) {
errors.email_address = 'Email is required'
} else if (!/\S+#\S+\.\S+/.test(values.email_address)) {
errors.email_address = 'Email is invalid'
}
//password validation
if (!values.password.trim()) {
errors.password = 'password is required'
} else if (values.password < 8) {
errors.password = 'PassWord need to be 8 characters or more'
}
return errors
}
async handleSubmit(event) {
if (event) {
event.preventDefault()
}
this.setState(
{ isSubmitting: true, errors: this.validation() },
async () => {
if (Object.keys(this.state.errors).length === 0) {
try {
const response = await this.props.login(this.state.values)
if (response) {
const redirect = Cookies.get('redirect')
if (redirect) {
this.setState({ redirect })
Cookies.remove('redirect')
} else {
this.setState({ redirect: '/' })
}
}
} catch (error) {
console.log(error)
} finally {
// this.setState({ isSubmitting: false })
}
} else {
this.setState({ isSubmitting: false })
}
}
)
}
render() {
return (
<View
onChange={this.handleChange}
values={this.state.values}
errors={this.state.errors}
errorMessage={this.props.errorMessage}
onSubmit={this.handleSubmit}
isSubmitting={this.state.isSubmitting}
redirect={this.state.redirect}
/>
)
}
}
const mapDispatchToProps = (dispatch) => {
return {
login: (values) => {
return dispatch(loginAction(values))
},
}
}
const mapStateToProps = (state) => {
return {
errorMessage: state.auth.errorMessage,
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Login)
AuthRoute component
import { logoutAction } from '../state/action-creators/AuthAction'
import { connect } from 'react-redux'
import { Navigate, Outlet } from 'react-router-dom'
import React from 'react'
import Cookies from 'js-cookie'
class AuthRoute extends React.Component {
render() {
const { authenticated, logout } = this.props
if (!authenticated) {
Cookies.set('redirect', window.location.pathname, { path: '/' })
logout()
return <Navigate to="/login" />
}
return <Outlet />
}
}
const mapDispatchToProps = (dispatch) => {
return {
logout: () => {
return dispatch(logoutAction())
},
}
}
const mapStateToProps = (state) => ({
authenticated: state.auth.authenticated,
})
export default connect(mapStateToProps, mapDispatchToProps)(AuthRoute)
Login action
import { formatError, login } from './AuthService'
export const CLEAR_ERROR_MESSAGE = '[register action] clear error message'
export const LOGIN_CONFIRMED_ACTION = '[login action] confirmed login'
export const LOGIN_FAILED_ACTION = '[login action] failed login'
export const LOGOUT_ACTION = '[logout action] logout action'
function clearErrorAction() {
return {
type: CLEAR_ERROR_MESSAGE,
}
}
function confirmedLoginAction(payload) {
return {
type: LOGIN_CONFIRMED_ACTION,
payload,
}
}
function failedLoginAction(message) {
return {
type: LOGIN_FAILED_ACTION,
payload: message,
}
}
export function loginAction({ email_address, password }) {
return (dispatch) => {
return login({
email_address,
password,
})
.then((response) => {
const token = response.data.access_token
localStorage.setItem('token', token)
dispatch(confirmedLoginAction(response))
return response
})
.catch((error) => {
if (!error.response) {
dispatch(failedLoginAction('Server error'))
} else {
const errorMessage = formatError(error.response.data)
dispatch(failedLoginAction(errorMessage))
}
throw error
})
}
}
function confirmedLogoutAction() {
return {
type: LOGOUT_ACTION,
}
}
export function logoutAction() {
return (dispatch) => {
localStorage.removeItem('token')
dispatch(confirmedLogoutAction())
}
}
AuthReducer
import {
LOGIN_CONFIRMED_ACTION,
LOGIN_FAILED_ACTION,
CLEAR_ERROR_MESSAGE,
LOGOUT_ACTION,
} from '../action-creators/AuthAction'
const initialState = {
user: {
first_name: '',
last_name: '',
user_name: '',
email_address: '',
password: '',
id: '',
is_archived: '',
projects: '',
date_created: '',
},
access_token: '',
errorMessage: '',
authenticated: !!localStorage.getItem('token'),
}
export function AuthReducer(state = initialState, action) {
if (action.type === CLEAR_ERROR_MESSAGE) {
return {
...state,
errorMessage: '',
}
}
if (action.type === LOGIN_CONFIRMED_ACTION) {
return {
...state,
authenticated: true,
user: action.payload,
}
}
if (action.type === LOGIN_FAILED_ACTION) {
return {
...state,
errorMessage: action.payload,
}
}
if (action.type === LOGOUT_ACTION) {
return {
...state,
authenticated: false,
user: {
first_name: '',
last_name: '',
user_name: '',
email_address: '',
password: '',
id: '',
is_archived: '',
projects: '',
date_created: '',
},
}
}
return state
}
I am getting error "react_devtools_backend.js:3973 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method." from react because I am trying to setState after login but the the guest route component have already redirected out from the Login component before setting the state hence the error.
Thank you in advance.
It seems the issue is that the loginAction action creator dispatches a login success action and the redux state is updated, which triggers a rerender. This rerender and updated authenticated state is used by the PublicRoute component and the user is bounced to the home route "/" before the redirect state update occurs in handleSubmit of the Login component.
As far as I can tell it appears you are using the redirect state in Login to pass a redirect prop to the View component to, I assume, render a Navigate component to redirect back to the referrer route.
This extra step/state update/rerender is completely extraneous and unnecessary. It is far more common to issue an imperative navigate from the login handler upon successful authentication. For this the navigate function is used to redirect back. Since the Login component is a class component it can't use the useNavigate hook. The options here are to either convert to function component or create a custom withNavigate HOC.
Example:
import { useNavigate } from 'react-router-dom';
const withNavigate = Component => props => {
const navigate = useNavigate();
return <Component {...props} navigate={navigate} />;
};
export default withNavigate;
Decorate the Login component with the withNavigate HOC and access the navigate function from props and issue the imperative redirect.
import React from 'react';
import { compose } from 'redux';
import { connect } from 'react-redux';
import Cookies from 'js-cookie';
import { loginAction } from '../state/action-creators/AuthAction';
import View from '../components/Login';
import withNavigate from '../path/to/withNavigate';
class Login extends React.Component {
constructor(props) {
super(props)
this.state = {
values: {
email_address: '',
password: '',
},
errors: {},
isSubmitting: false,
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
...
async handleSubmit(event) {
if (event) {
event.preventDefault();
}
this.setState(
{ isSubmitting: true, errors: this.validation() },
async () => {
if (!Object.keys(this.state.errors).length) {
try {
const response = await this.props.login(this.state.values);
if (response) {
const redirect = Cookies.get('redirect');
this.props.navigate(redirect || "/", { replace: true });
}
} catch (error) {
console.log(error);
// didn't authenticate, clear submitting
this.setState({ isSubmitting: false });
}
} else {
this.setState({ isSubmitting: false });
}
}
);
}
render() {
return (
<View
onChange={this.handleChange}
values={this.state.values}
errors={this.state.errors}
errorMessage={this.props.errorMessage}
onSubmit={this.handleSubmit}
isSubmitting={this.state.isSubmitting}
/>
)
}
}
const mapDispatchToProps = {
login: loginAction,
};
const mapStateToProps = (state) => ({
errorMessage: state.auth.errorMessage,
});
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withNavigate,
)(Login);
Could someone please let me know why the state isn't being updated from the reducer? The useEffect(()=>{}) isn't being triggered when the state is being returned from the reducer. I have validated the correct information is being passed to the return, but nothing can be seen from the LoginScreen.
Context Script
import React, { createContext, useReducer } from "react";
import userReducer from "./UserReducer";
export const UserContext = createContext();
const initialState = {
userData: [],
isLoggedIn: false,
isAdmin: false,
isEmployee: false,
errorMessage: [{ success: false, statusCode: 0, error: null }],
};
const UserContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(userReducer, initialState);
const registerUser = (user) =>
dispatch({ type: "REGISTER_USER", payload: user });
const loginUser = (user) => dispatch({ type: "LOGIN_USER", payload: user });
const deleteUser = (user) => dispatch({ type: "DELETE_USER", payload: user });
const updateUser = (user) => dispatch({ type: "UPDATE_USER", payload: user });
const contextValues = {
...state,
registerUser,
loginUser,
deleteUser,
updateUser,
};
return (
<UserContext.Provider value={contextValues}>
{children}
</UserContext.Provider>
);
};
export default UserContextProvider;
Reducer Script
import axios from "axios";
axios.defaults.withCredentials = true;
const userReducer = (state = {}, action) => {
let config = {
header: {
"Content-Type": "application/json",
},
};
switch (action.type) {
case "REGISTER_USER":
break;
case "LOGIN_USER":
console.log(state);
const email = action.payload.email;
const password = action.payload.password;
axios
.post("/api/user/login", { email, password }, config)
.then((response) => {
if (response.data.success) {
// localStorage.setItem("authToken", response.data.authToken);
state.userData = response.data.user;
state.isLoggedIn = true;
if (response.data.user.role === 9) {
state.isAdmin = true;
state.isEmployee = true;
} else {
state.isAdmin = false;
state.isEmployee = false;
}
}
})
.catch((error) => {
state.errorMessage = {
success: error.response.data.success,
statusCode: error.response.status,
message: error.response.data.error,
};
});
return {
...state,
userData: [state.userData],
isLoggedIn: state.isLoggedIn,
isAdmin: state.isAdmin,
isEmployee: state.isEmployee,
errorMessage: [state.errorMessage],
};
default:
return state;
}
};
export default userReducer;
Login Form
import { useState, useEffect, useContext } from "react";
import { Link } from "react-router-dom";
import {
Button,
Form,
Grid,
Message,
Segment,
Image,
Container,
} from "semantic-ui-react";
//Custom Imports
import "./LoginScreen.css";
import Logo from "../../../img/logo.png";
//Context
import { UserContext } from "../../context/UserContext";
const LoginScreen = ({ history }) => {
const { userData, loginUser, isLoggedIn, errorMessage, clearErrorMessage } =
useContext(UserContext);
const [user, setUser] = useState({ email: "", password: "" });
const [error, setError] = useState("");
useEffect(() => {
console.log(errorMessage);
if (localStorage.getItem("authToken")) {
history.push("/dashboard");
}
}, [history]);
useEffect(() => {
if (isLoggedIn) {
console.log(userData);
console.log("User is Logged in");
// history.push("/");
}
if (!errorMessage.success && errorMessage.error != null) {
console.log(errorMessage);
setError(errorMessage.message);
setTimeout(() => {
setError("");
}, 5000);
}
}, [userData, errorMessage, isLoggedIn]);
return (
<Container className="login-container">
<Grid
textAlign="center"
style={{ height: "100vh" }}
verticalAlign="middle"
>
<Grid.Column style={{ maxWidth: 450 }}>
<Image src={Logo} className="login-logo" />
<Form size="large" onSubmit={() => loginUser(user)}>
<Segment stacked>
<Form.Input
fluid
icon="user"
iconPosition="left"
placeholder="Email Address"
value={user.email}
onChange={(e) => setUser({ ...user, email: e.target.value })}
/>
<Form.Input
fluid
icon="lock"
iconPosition="left"
placeholder="Password"
value={user.password}
type="password"
onChange={(e) => setUser({ ...user, password: e.target.value })}
/>
{error && <span>{error}</span>}
<Button color="blue" fluid size="large" type="submit">
Login
</Button>
</Segment>
</Form>
<Message>
Don't have an account? <Link to="/register">Sign Up</Link>
</Message>
</Grid.Column>
</Grid>
</Container>
);
};
export default LoginScreen;
Refactor your login function like this
const loginUser({ email, password }) => {
let config = {
header: {
"Content-Type": "application/json",
},
};
axios
.post("/api/user/login", { email, password }, config)
.then((response) => {
if (response.data.success) {
dispatch({ type: 'LOGIN_SUCCESS', payload: response.data });
}
})
.catch((error) => {
dispatch({ type: 'LOGIN_FAILED', payload: error });
});
}
and then your reducer
...
switch(action.type) {
...
case 'LOGIN_SUCCESS':
// return here a new object
// do not mutate the state (state.something = something) is not allowed
...
case 'LOGIN_FAILED':
// handle error
}
Prerequisite Reducer Concepts
Redux and useReducer use reducer like (previousState, action) => newState.
The reducer should be a 'pure' function as in this document. The promises, api calls should not be use inside reducers.
The problem:
Because you call api/promise inside the reducer. The reducer function returns the value before the promise finish. So when the promise finishes, nothing happen.
// A will be return before B, C are going to call
case "LOGIN_USER":
promiseFn()
.then(/* B */ ...)
.catch(/* C */ ...)
// A
return {
...
}
Solution:
Separate the non-pure calls from the reducer. And put them in the other code blocks (like inside hooks, event handlers...).