skip to Main Content

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?&nbsp;
                <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


  1. your issue is in here

    
     const handleLogin = async (credentials) => {
        await dispatch(loginUser(credentials));
        // console.log(isAuthenticated);
        // const {
        //   auth: { isAuthenticated },
        // } = store.getState();
    
        if (isAuthenticated) {
          navigate("/");
        }
      };
    
    

    though you have used await on dispatch, it wont actually wait till reducer has finish its modification. therefore on second hit, "isAuthenticated" is indeed true already and you will be navigated.

    the right approach should use useEffect with isAuthenticated as trigger

    const handleLogin = (credentials) => {
        dispatch(loginUser(credentials));
    }
    
    useEffect(()=>{ isAuthenticated && navigate("/") }, [ isAuthenticated ])
    
    Login or Signup to reply.
  2. 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
      }
    };
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search