When I try to log in, the Redux reducers update the state correctly, but the login page doesn’t navigate to the home page immediately. However, if I click the login button again, then it navigates to the home page. The backend seems to be working fine, and when I check with Redux-DevTools, it shows that the state has been updated correctly. When I login, the isAuthenticated
outside of the handleLogin
logs out true
, but the isAuthenticated
inside the handleLogin
logs out false
.
Login:
import React, { useEffect } from "react";
import "./login.css";
import store from "../../store/store";
import { Form, Formik, Field } from "formik";
import { TextField, Button, CircularProgress } from "@mui/material";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import { loginUser } from "../../store/auth/auth.actions";
import * as Yup from "yup";
export default function Login() {
const navigate = useNavigate();
const dispatch = useDispatch();
const isAuthenticated = useSelector((state) => state.auth.isAuthenticated);
// console.log(isAuthenticated);
const handleLogin = async (credentials) => {
await dispatch(loginUser(credentials));
// console.log(isAuthenticated);
// const {
// auth: { isAuthenticated },
// } = store.getState();
if (isAuthenticated) {
navigate("/");
}
};
const validationSchema = Yup.object().shape({
username: Yup.string().required("Username is required"),
password: Yup.string().required("Password is required"),
});
return (
<main className="login-page">
<section className="picture-container">
<img
src={process.env.PUBLIC_URL + "/images/login.jpg"}
alt="decoration"
/>
</section>
<section className="login-section">
<section className="login-title">
<h1>Login</h1>
<h3>Log into CMUniversity</h3>
</section>
<Formik
initialValues={{ username: "", password: "" }}
validationSchema={validationSchema}
initialErrors={{ username: "" }}
onSubmit={async (values) => {
await handleLogin(values);
}}
>
{({ errors, touched, isValid }) => (
<Form className="login-form">
<Field
as={TextField}
type="text"
label="Username"
name="username"
error={touched.username && !!errors.username}
helperText={touched.username && errors.username}
required
className="form-group"
/>
<Field
as={TextField}
type="password"
label="Password"
name="password"
error={touched.password && !!errors.password}
helperText={touched.password && errors.password}
className="form-group"
required
/>
<span className="credentials">
Forgot your <span className="active">username</span> or{" "}
<span className="active">password</span>
</span>
<Button
type="submit"
variant="contained"
className="submitButton"
disabled={!isValid}
>
{/* {loading ? (<CircularProgress />):("Log in")} */}
Log In
</Button>
{/* Display error message if login failed */}
{/* {error && <p className="error-message">{error}</p>} */}
<section className="social-login">
<p>Or log in using: </p>
<Button
type="button"
variant="contained"
className="googleButton"
>
<img
src={process.env.PUBLIC_URL + "/images/google.png"}
alt="Sign in with Google"
/>
</Button>
<Button
type="button"
variant="contained"
className="facebookButton"
>
<img
src={process.env.PUBLIC_URL + "/images/facebook.png"}
alt="Sign in with Facebook"
/>
</Button>
</section>
<span className="credentials">
Not a member yet?
<span className="active">Sign Up Now</span>{" "}
</span>
</Form>
)}
</Formik>
</section>
</main>
);
}
Actions to dispatch:
export const loginUser = createAsyncThunk(
"auth/loginUser",
async (credentials, thunkApi) => {
try {
const { data } = await API.post("auth/login", credentials)
return data;
} catch (err) {
return thunkApi.rejectWithValue(err.message);
}
}
);
Reducers:
import { createSlice } from "@reduxjs/toolkit";
import { loginUser } from "./auth.actions.js";
const authSlice = createSlice({
name: "auth",
initialState: {
user: null,
isAuthenticated: false,
error: null,
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(loginUser.pending, (state, action) => {
state.user = null;
state.isAuthenticated = false;
state.error = null;
})
.addCase(loginUser.fulfilled, (state, action) => {
state.user = action.payload;
state.isAuthenticated = true;
state.error = null;
})
.addCase(loginUser.rejected, (state, action) => {
state.user = null;
state.isAuthenticated = false;
state.error = action.payload;
});
},
});
// Export reducer function by default
export default authSlice.reducer;
2
Answers
your issue is in here
though you have used
await
ondispatch
, it wont actually wait till reducer has finish its modification. therefore on second hit, "isAuthenticated" is indeedtrue
already and you will be navigated.the right approach should use
useEffect
withisAuthenticated
as triggerThe issue is that
handleLogin
has a closure over the selectedisAuthenticated
value from the time it’s called, it won’t ever be a different or updated value in the callback.The
loginUser
action either is fulfilled or is rejected. ThehandleLogin
callback canawait
this action being fulfilled. All Redux-Toolkit thunks resolve, so the key is to first unwrap the resolved result to see if it was fulfilled or rejected.See Handling Thunk Results for details.