Search code examples
javascriptreactjsecmascript-6react-reduxredux-toolkit

Doesnt update state after successfully fulfilled Reducers


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;

Solution

  • 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
      }
    };