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...).
Related
Im using react js with redux/toolkit to create user authentication.
I created a form with Formik. When the I submit the form it doesn't redirect me to the dashboard aand I'm unable to identify the issue.
I created a default authentication service under src/services/AuthService.js, the service method works coresponded useAuth fuction.
AuthService.js
export async function apiSignIn (data) {
return ApiService.fetchData({
url: '/v1/auth/login',
method: 'post',
data
})
}
And then I created a store with to Slices:
1- sessionsSlice
2- userSlice:
The logic I implemented in : src/store/auth/sessionSlice.js
import { createSlice } from '#reduxjs/toolkit'
export const sessionSlice = createSlice({
name: 'auth/session',
initialState: {
token: '',
signedIn: false,
},
reducers: {
onSignInSuccess: (state, action) => {
state.signedIn = true
state.token = action.payload
},
onSignOutSuccess: (state) => {
state.signedIn = false
state.token = ''
},
setToken: (state, action) => {
state.token = action.payload
}
},
})
export const { onSignInSuccess, onSignOutSuccess, setToken } = sessionSlice.actions
export default sessionSlice.reducer
The logic I implemented in : src/store/auth/userSlice .js
import { createSlice } from '#reduxjs/toolkit'
export const initialState = {
avatar: '',
userName: '',
email: '',
authority: []
}
export const userSlice = createSlice({
name: 'auth/user',
initialState,
reducers: {
setUser: (_, action) => action.payload,
userLoggedOut: () => initialState,
},
})
export const { setUser } = userSlice.actions
export default userSlice.reducer
The signIn logic implementation in the useAuth looks this way:
import { useSelector, useDispatch } from 'react-redux'
import { setUser, initialState } from 'store/auth/userSlice'
import { apiSignIn, apiSignOut, apiSignUp } from 'services/AuthService'
import { onSignInSuccess, onSignOutSuccess } from 'store/auth/sessionSlice'
import { REDIRECT_URL_KEY } from 'constants/app.constant'
import { useNavigate } from 'react-router-dom'
import useQuery from './useQuery'
function useAuth() {
const dispatch = useDispatch()
const navigate = useNavigate()
const query = useQuery()
const { token, signedIn } = useSelector((state) => state.auth.session)
const signIn = async (values) => {
try {
const resp = await apiSignIn(values)
if (resp.data) {
const { token } = resp.data
dispatch(onSignInSuccess(token))
if(resp.data) {
dispatch(setUser(resp.data || {
avatar: '',
userName: resp.data.userName,
authority: [resp.data.role],
email: resp.data.email
}))
}
const redirectUrl = query.get(REDIRECT_URL_KEY)
navigate(redirectUrl ? redirectUrl : '/dashboard')
return {
status: 'success',
message: ''
}
}
} catch (errors) {
return {
status: 'failed',
message: errors?.response?.data?.message || errors.toString()
}
}
}
const signUp = async (values) => {
try {
const resp = await apiSignUp(values)
if (resp.data) {
const { token } = resp.data
dispatch(onSignInSuccess(token))
if(resp.data.user) {
dispatch(setUser(resp.data.user || {
avatar: '',
userName: 'Anonymous',
authority: ['USER'],
email: ''
}))
}
const redirectUrl = query.get(REDIRECT_URL_KEY)
navigate(redirectUrl ? redirectUrl : '/dashboard')
return {
status: 'success',
message: ''
}
}
} catch (errors) {
return {
status: 'failed',
message: errors?.response?.data?.message || errors.toString()
}
}
}
const handleSignOut = () => {
dispatch(onSignOutSuccess())
dispatch(setUser(initialState))
navigate('/dashboard')
}
const signOut = async () => {
await apiSignOut()
handleSignOut()
}
return {
authenticated: token && signedIn,
signIn,
signUp,
signOut
}
}
export default useAuth
And finally, My form looks this way:
import React from 'react'
import { Input, Button, Checkbox, FormItem, FormContainer, Alert } from 'components/ui'
import { PasswordInput, ActionLink } from 'components/shared'
import useTimeOutMessage from 'utils/hooks/useTimeOutMessage'
import { Field, Form, Formik } from 'formik'
import * as Yup from 'yup'
import useAuth from 'utils/hooks/useAuth'
const validationSchema = Yup.object().shape({
userName: Yup.string().required('Please enter your username'),
password: Yup.string().required('Please enter your password'),
rememberMe: Yup.bool()
})
const SignInForm = props => {
const {
disableSubmit = false,
className,
} = props
const [message, setMessage] = useTimeOutMessage()
const { signIn } = useAuth()
const onSignIn = async (values, setSubmitting) => {
const { userName, password } = values
setSubmitting(true)
const result = await signIn({ userName, password })
if (result.status === 'failed') {
setMessage(result.message)
}
setSubmitting(false)
}
return (
<div className={className}>
{message && <Alert className="mb-4" type="danger" showIcon>{message}</Alert>}
<Formik
initialValues={{
userName: '',
password: '',
rememberMe: true
}}
validationSchema={validationSchema}
onSubmit={(values, { setSubmitting }) => {
if(!disableSubmit) {
onSignIn(values, setSubmitting)
} else {
setSubmitting(false)
}
}}
>
{({touched, errors, isSubmitting}) => (
<Form>
<FormContainer>
<FormItem
label="User Name"
invalid={errors.userName && touched.userName}
errorMessage={errors.userName}
>
<Field
type="text"
autoComplete="off"
name="userName"
placeholder="User Name"
component={Input}
/>
</FormItem>
<FormItem
label="Password"
invalid={errors.password && touched.password}
errorMessage={errors.password}
>
<Field
autoComplete="off"
name="password"
placeholder="Password"
component={PasswordInput}
/>
</FormItem>
<div className="flex justify-between mb-6">
<Field className="mb-0" name="rememberMe" component={Checkbox} children="Remember Me" />
</div>
<Button block loading={isSubmitting} variant="solid" type="submit">
{ isSubmitting ? 'Signing in...' : 'Sign In' }
</Button>
</FormContainer>
</Form>
)}
</Formik>
</div>
)
}
export default SignInForm
i want to implement update user password form using react-router-dom v6 but this code is not working..
please please.. put your suggestion or explain me about my mistakes on this code.
userReducer.js
import {
UPDATE_PASSWORD_REQUEST,
UPDATE_PASSWORD_SUCCESS,
UPDATE_PASSWORD_RESET,
UPDATE_PASSWORD_FAIL,
CLEAR_ERRORS,
} from "../Constants/userConstant";
export const profileReducer = (state = {}, action) => {
switch (action.type) {
case UPDATE_PASSWORD_REQUEST:
return {
...state,
loading: true,
};
case UPDATE_PASSWORD_SUCCESS:
return {
...state,
loading: false,
isUpdated: action.payload,
};
case UPDATE_PASSWORD_FAIL:
return {
...state,
loading: false,
error: action.payload,
};
case UPDATE_PASSWORD_RESET:
return {
...state,
isUpdated: false,
};
case CLEAR_ERRORS:
return {
...state,
error: null,
};
default:
return state;
}
};
userAction.js
import {
UPDATE_PASSWORD_REQUEST,
UPDATE_PASSWORD_SUCCESS,
UPDATE_PASSWORD_FAIL,
CLEAR_ERRORS,
} from "../Constants/userConstant";
export const updatePassword = (passwords) => async (dispatch) => {
try {
dispatch({ type: UPDATE_PASSWORD_REQUEST });
const config = { headers: { "Content-Type": "application/json" } };
const { data } = await axios.put(
`/api/v1/password/update`,
passwords,
config
);
dispatch({ type: UPDATE_PASSWORD_SUCCESS, payload: data.success });
} catch (error) {
dispatch({
type: UPDATE_PASSWORD_FAIL,
payload: error.response.data.message,
});
}
};
export const clearErrors = () => async (dispatch) => {
dispatch({ type: CLEAR_ERRORS });
};
store.js
import {createStore,combineReducers,applyMiddleware} from 'redux';
import thunk from "redux-thunk";
import {composeWithDevTools} from "redux-devtools-extension";
import { profileReducer } from './Reducers/userReducer';
const reducer = combineReducers({
profile:profileReducer,
})
let initialState = {};
const middleware = [thunk];
const store = createStore(
reducer,
initialState,
composeWithDevTools(applyMiddleware(...middleware))
);
export default store;
UpdatePassword.js
import React, { Fragment, useState, useEffect } from "react";
import "./UpdatePassword.css";
import Loader from "../Loader/Loader";
import { useDispatch, useSelector } from "react-redux";
import { clearErrors, updatePassword } from "../../Actions/userAction";
import { UPDATE_PASSWORD_RESET } from "../../Constants/userConstant";
import {useNavigate} from 'react-router-dom'
const UpdatePassword = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const { error, isUpdated, loading } = useSelector((state) => state.profile);
const [oldPassword, setOldPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const updatePasswordSubmit = (e) => {
e.preventDefault();
const myForm = new FormData();
myForm.set("oldPassword", oldPassword);
myForm.set("newPassword", newPassword);
myForm.set("confirmPassword", confirmPassword);
dispatch(updatePassword(myForm));
};
useEffect(() => {
if (error) {
alert(error);
dispatch(clearErrors());
}
if (isUpdated) {
alert("Profile Updated Successfully");
navigate("/account");
dispatch({
type: UPDATE_PASSWORD_RESET,
});
}
}, [dispatch, error, isUpdated]);
return (
<Fragment>
{loading ? (
<Loader />
) : (
<Fragment>
{/* <MetaData title="Change Password" /> */}
<div className="updatePasswordContainer">
<div className="updatePasswordBox">
<h2 className="updatePasswordHeading">Update Profile</h2>
<form
className="updatePasswordForm"
onSubmit={updatePasswordSubmit}
>
<div className="loginPassword">
<input
type="password"
placeholder="Old Password"
required
value={oldPassword}
onChange={(e) => setOldPassword(e.target.value)}
/>
</div>
<div className="loginPassword">
<input
type="password"
placeholder="New Password"
required
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
/>
</div>
<div className="loginPassword">
<input
type="password"
placeholder="Confirm Password"
required
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
</div>
<input
type="submit"
value="Change"
className="updatePasswordBtn"
/>
</form>
</div>
</div>
</Fragment>
)}
</Fragment>
);
};
export default UpdatePassword;
i want to make a form where user update user password.but due to any mistake this form is not working...
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'));
}
};
I'm not sure if the problem is in useSelector or in useDispatch hooks or in another place, so here is the scenario:
Two screens (HomeScreen & AddBlogScreen)
In HomeScreen I click add blog button then it redirect to AddBlogScreen
I input the data, then submit. After the submit is success then redirect to HomeScreen
As mentioned in below pic, I got the no 4 result & I have to refresh to get the no 3 result. But my expectation is no 3 pic without getting the error.
Here is my code:
HomeScreen
import jwtDecode from "jwt-decode";
import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useHistory } from "react-router";
import { blogList } from "../redux/action";
export const MainScreen = () => {
// const [token, setToken] = useState(localStorage.getItem("token"));
const user = jwtDecode(localStorage.getItem("token"));
const history = useHistory();
const dispatch = useDispatch();
useEffect(() => {
dispatch(blogList());
}, [dispatch]);
const { blog } = useSelector((state) => state.blog);
console.log(blog);
return (
<>
<button
onClick={() => {
localStorage.removeItem("token");
history.push("/");
}}
>
singout
</button>
<button
onClick={() => {
history.push({ pathname: "/Blog", state: user });
}}
>
add blog
</button>
<h1 style={{ color: "red" }}>username: {user.username}</h1>
{blog.map(({ id, b_title, b_content, category_id }) => (
<div key={id}>
<h1
onClick={() =>
history.push({
pathname: "/Edit",
state: { id, b_title, b_content, category_id },
})
}
>
Title: {b_title}
</h1>
<p>Content: {b_content}</p>
</div>
))}
</>
);
};
AddBlogScreen
import React, { useState } from "react";
import { useDispatch } from "react-redux";
import { useHistory, useLocation } from "react-router";
import { addBlog } from "../redux/action";
export const AddBlogScreen = () => {
const history = useHistory();
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [category, setCategory] = useState("");
const dispatch = useDispatch();
const location = useLocation();
const author = location.state.id;
const submitHandler = (e) => {
e.preventDefault();
dispatch(addBlog(title, content, author, category));
setTitle("");
setContent("");
setCategory("");
history.push("/Home");
};
return (
<div>
<h1>add blog page</h1>
<form onSubmit={submitHandler}>
<input
type="text"
placeholder="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<br />
<br />
<input
type="text"
placeholder="content"
value={content}
onChange={(e) => setContent(e.target.value)}
/>
<br />
<br />
<input
type="text"
placeholder="category"
value={category}
onChange={(e) => setCategory(e.target.value)}
/>
<br />
<br />
<input
type="submit"
value="submit"
disabled={
title === "" || content === "" || category === "" ? true : false
}
/>
</form>
</div>
);
};
actions
import axios from "axios";
import {
LIST_BLOG,
ADD_BLOG,
EDIT_BLOG,
DELETE_BLOG,
LOGIN_USER,
REGISTER_USER,
LOGOUT_USER,
} from "./constant";
// ==================== blog actions ======================
export const blogList = () => async (dispatch) => {
try {
const result = await axios
.get("http://localhost:3001/api/v1/blog?page=0")
.then((res) => res.data.data)
.catch((err) => err);
dispatch({
type: LIST_BLOG,
payload: result,
});
} catch (err) {
dispatch({
payload: err,
});
}
};
export const addBlog =
(title, content, author, category) => async (dispatch) => {
try {
const result = await axios
.post("http://localhost:3001/api/v1/blog", {
blog_title: title,
blog_content: content,
author_id: author,
category_id: category,
})
.then(alert("success add blog"))
.catch((err) => alert(err));
dispatch({
type: ADD_BLOG,
payload: result,
});
} catch (err) {
dispatch({
payload: err,
});
}
};
reducer
const initial_state = {
blog: [],
};
export const blogReducer = (state = initial_state, action) => {
switch (action.type) {
case LIST_BLOG:
return {
...state,
blog: action.payload,
};
case ADD_BLOG:
return {
...state,
blog: action.payload,
};
case EDIT_BLOG:
return {
...state,
blog: action.payload,
};
case DELETE_BLOG:
return {
...state,
blog: action.payload,
};
default:
return state;
}
};
store
import { blogReducer, userReducer } from "./reducer";
import { combineReducers, createStore, applyMiddleware } from "redux";
import { composeWithDevTools } from "redux-devtools-extension";
import thunk from "redux-thunk";
const reducer = combineReducers({
blog: blogReducer,
user: userReducer,
});
const middleWare = composeWithDevTools(applyMiddleware(thunk));
export const store = createStore(reducer, middleWare);
First of all, the origin of error:
the error says a property named map on blog is not a function, meaning blog is not an array.
This is where it is coming from:
const { blog } = useSelector((state) => state.blog);
Your state is a an ojbect with a property named blog, you can access it these two ways:
const { blog } = useSelector((state) => state);
or
const blog = useSelector((state) => state.blog);
Other issues I noticed :
in addBlog:
1. When you are using try-catch with await, it's not a good idea to use then-catch too.
2.result won't be the blog data you expect. It will be an object, which is an instance of AxiosResponse, which includes the data.
you can extract the data from response object this way:
let response = await axios.post(... // some api request
let {data}=response
I would edit it like this:
export const addBlog =
(title, content, author, category) => async (dispatch) => {
try {
const {data} = await axios
.post("http://localhost:3001/api/v1/blog", {
blog_title: title,
blog_content: content,
author_id: author,
category_id: category,
})
alert("success add blog")
dispatch({
type: ADD_BLOG,
payload: data,
});
} catch (err) {
dispatch({
payload: err,
});
}
};
I found the solution, so in my action I changed it into:
dispatch({
type: LIST_BLOG,
payload: result.data.data,
});
I create a app, everything works fine, but when i move route or reload the page, the state back to the default situation, maybe i able to fix this problem using library as react-persist or found some way to put the state on the local Storage, i don't think this approach is the best way.Maybe you another alternative?
`store.js`
import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import rootReducer from "./reducer/rootReducer";
const initialState = {};
export default createStore(rootReducer, initialState, applyMiddleware(thunk));
`reducer`
import {
REGISTER_USER,
REGISTER_FAIL,
LOGIN_USER,
LOGIN_FAIL,
// LOAD_USER,
// LOAD_USER_FAIL,
// LOGOUT_USER,
} from "../constants";
const initialState = {
user: null,
loading: true,
isAuthenticated: false,
error: null,
};
export default (state = initialState, action) => {
switch (action.type) {
case REGISTER_USER:
return {
...state,
user: action.payload,
loading: false,
isAuthenticated: true,
error: null,
};
case REGISTER_FAIL:
return {
...state,
user: null,
loading: true,
isAuthenticated: false,
error: action.payload,
};
case LOGIN_USER:
localStorage.setItem("token", action.payload.token);
return {
...state,
user: action.payload,
loading: false,
isAuthenticated: true,
error: null,
};
case LOGIN_FAIL:
return {
...state,
user: null,
loading: true,
isAuthenticated: false,
error: action.payload,
};
default:
return state;
}
};
`Login`
import React, { useState, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Redirect, useHistory } from "react-router-dom";
import { loginAction } from "../actions/loginAction";
const Login = () => {
const dispatch = useDispatch();
const data = useSelector((state) => state.auth);
let history = useHistory();
const [msg, setMsg] = useState("");
const [state, setState] = useState({
email: "",
password: "",
});
useEffect(() => {
let interval;
if (data.error) {
setMsg(data.error.msg);
interval = setTimeout(() => {
setMsg("");
}, 1000);
}
if (data.user) {
setMsg(data.user.msg);
interval = setTimeout(() => {
setMsg("");
}, 1000);
}
return () => {
clearTimeout(interval);
};
}, [data]);
const handleSubmit = (e) => {
e.preventDefault();
dispatch(loginAction(state));
};
const handleChange = (e) => {
setState({ ...state, [e.target.name]: e.target.value });
};
if (data.isAuthenticated) {
history.push("/dashboard");
}
console.log(data);
return (
<>
<span>{msg}</span>
<h1>Login</h1>
<form onSubmit={handleSubmit}>
<input name="email" onChange={handleChange} value={state.email} />
<input name="password" onChange={handleChange} value={state.password} />
<input type="submit" value="Login" />
</form>
</>
);
};
export default Login;
register
import React, { useState, useEffect } from "react";
import { registerAction } from "../actions/registerAction";
import { useDispatch, useSelector } from "react-redux";
import { Redirect } from "react-router-dom";
const Register = () => {
const dispatch = useDispatch();
const data = useSelector((state) => state.auth);
const [msg, setMsg] = useState("");
const [state, setState] = useState({
name: "",
email: "",
password: "",
photo: "",
});
useEffect(() => {
let interval;
if (data.error) {
setMsg(data.error);
interval = setTimeout(() => {
setMsg("");
}, 1000);
}
if (data.user) {
setMsg(data.user.msg);
interval = setTimeout(() => {
setMsg("");
}, 1000);
}
return () => {
clearTimeout(interval);
};
}, [data]);
if (data.user) {
return <Redirect to="/login" />;
}
const handleSubmit = (e) => {
e.preventDefault();
const formData = new FormData();
formData.append("name", state.name);
formData.append("email", state.email);
formData.append("password", state.password);
formData.append("photo", state.photo);
dispatch(registerAction(formData));
};
const handleChange = (e) => {
setState({ ...state, [e.target.name]: e.target.value });
};
const handleOnUploadFile = (e) => {
setState({ ...state, photo: e.target.files[0] });
};
return (
<>
<span>{msg}</span>
<form onSubmit={handleSubmit}>
<label>Name</label>
<input name="name" onChange={handleChange} value={state.name} />
<label>Email</label>
<input name="email" onChange={handleChange} value={state.email} />
<label>Password</label>
<input name="password" onChange={handleChange} value={state.password} />
<input
name="photo"
type="file"
accept="image/*"
onChange={handleOnUploadFile}
/>
<input type="submit" />
</form>
</>
);
};
export default Register;
You should store a token in Local storage and pull it out and verify it upon reload. Also you should make a new action to load user from that token. States will alwayr reset to initial state on reload.
I cant see ur registerAction page but the loadUser should be something like this
// Load User
export const loadUser = () => async (dispatch) => {
try {
const res = await axios.get('/api/auth');
dispatch({
type: USER_LOADED,
payload: res.data,
} catch (err) {
dispatch({
type: AUTH_ERROR,
payload: err.response.data,
});
}
};
/api/auth is like this
// #route GET api/auth
// #desc Get logged in user (Login)
// #access Private
router.get('/', auth, async (req, res) => {
try {
const user = await Planner.findById(req.user.id).select('-password');
if (user.type === 'Admin') {
res.json({ user, admin: true });
} else {
res.json({ user, admin: false });
}
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
});
You need to use jwttoken for example as an auth middle ware
the middleware file would be like this
const jwt = require('jsonwebtoken');
const config = require('config');
module.exports = function (req, res, next) {
//Get token from header
const token = req.header('x-auth-token');
// Check if not token
if (!token) {
return res.status(401).json({ msg: 'No token, authorization denied' });
}
try {
const decoded = jwt.verify(token, config.get('jwtSecret'));
req.user = decoded.user;
next();
} catch (err) {
res.status(401).json({ msg: 'Token is not valid' });
}
};
I have implemented a similar redux of the contact keeper project by Brad Travery
You can see the full code here am sure it will help
https://github.com/madaher-dev/contact-keeper
Enjoy