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;
The issue is that handleLogin
has a closure over the selected isAuthenticated
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. The handleLogin
callback can await
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.
const handleLogin = async (credentials) => {
try {
await dispatch(loginUser(credentials)).unwrap();
// Success 😀, navigate home
navigate("/");
} catch(error) {
// Failure 🙁, handle error
// The error is the returned `thunkApi.rejectWithValue(err.message)` value
}
};