2

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;

enter image description here

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;

  • You shouldn't do side effects in your reducer, nor should you mutate the state. That is most likely the root cause of the misbehaviour. Do the fetching in the useEffect, and then dispatch an action with the payload, and return the new state with the updated data – shidoro Jan 17 '22 at 00:05

2 Answers2

0

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
}

shidoro
  • 362
  • 4
  • 9
0

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...).

Long Nguyen Duc
  • 1,317
  • 1
  • 8
  • 13