Search code examples
reactjsexpressreduxredux-thunk

react & redux with hooks: Actions must be plain objects. Use custom middleware for async actions


i tried looking for similar answers to help solve my problem but i couldn't find anything using react redux hooks. This code was from a tutorial and originally written using the Context api. I wanted to trying using it with react-redux-hooks, but i got stuck. Basically i'm trying to register a user with a name, email and password, then pass these three as an object to the express server which will validated it and give me back a jwt token. Then come back to the client side and send the token to the reducer, which adds the token to localstorage and sets the state to isAuthenticated. The error i get is on the dispatch.

Dispatch

const onSubmit = e => {
 e.preventDefault();

 if (name === "" || email === "" || password === "") {
  dispatch(setAlert("Please enter all fields", "danger"));
 } else if (password !== password2) {
  dispatch(setAlert("Passwords do not match", "danger"));
 } else {
  dispatch(register({ name, email, password })); // Error is here
 }

 setTimeout(() => {
  dispatch(removeAlert());
 }, 5000);
};

Action

 export const register = async formData => {
  const config = {
  headers: {
  "Content-Type": "application/json"
  }
 };

try {
const res = await axios.post("/api/users", formData, config);

return {
  type: "REGISTER_SUCCESS",
  payload: res.data
  };
} catch (err) {
return {
  type: "REGISTER_FAIL",
  payload: err.response.data.msg
  };
 }
};

Reducer

const authReducer = (
 state = {
 token: localStorage.getItem("token"),
 isAuthenticated: null,
 loading: true,
 user: null,
 error: null
},
action
) => {
 switch (action.type) {
  case "REGISTER_SUCCESS":
   console.log("register success");
   localStorage.setItem("token", action.payload.token);
   return {
    ...state,
    ...action.payload,
    isAuthenticated: true,
    loading: false
   };

 case "REGISTER_FAIL":
   console.log("register failed");
   localStorage.removeItem("token");
   return {
    ...state,
    token: null,
    isAuthenticated: false,
    loading: false,
    user: null,
    error: action.payload
   };

 default:
   return state;
 }
};

Store

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ 
|| compose;

 const store = createStore(
 allReducers,
 composeEnhancers(applyMiddleware(thunk))
);

ReactDOM.render(
  <Provider store={store}>
   <App />
  </Provider>,
 document.getElementById("root")
);

Express server

router.post(
 "/",
  [
  check("name", "Please a name")
    .not()
    .isEmpty(),
  check("email", "Please include a valid email").isEmail(),
  check(
    "password",
    "Please enter a password with 6 or more characters"
   ).isLength({
     min: 6
   })
 ],
async (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({
      errors: errors.array()
    });
  }

  const { name, email, password } = req.body;

  try {
   let user = await User.findOne({ email });

  if (user) {
    return res.status(400).json({
      msg: "User already exists"
    });
  }

  user = new User({
    name,
    email,
    password
  });

  // hash passsword
  const salt = await bcrypt.genSalt(10);

  user.password = await bcrypt.hash(password, salt);

  await user.save();

  const payload = {
    user: {
      id: user.id
    }
  };

  jwt.sign(
    payload,
    config.get("jwtSecret"),
    {
      expiresIn: 360000
    },
    (err, token) => {
      if (err) throw err;
      res.json({
        token
      });
    }
  );
} catch (err) {
  console.error(err.message);
  res.status(500).send("Server Error");
 }
}
);

Solution

  • I believe this question has answers to the issue your experiencing here: how to async/await redux-thunk actions?

    Using this example, it may look something like this (wasn't able to test it):

    export const register = formData => {
      const config = {
        headers: {
          "Content-Type": "application/json"
        }
      };
    
      const request = axios.post("/api/users", formData, config);
    
      return dispatch => {
        const onSuccess = success => {
          dispatch({
            type: "REGISTER_SUCCESS",
            payload: success.data
          });
          return success;
        };
        const onError = error => {
          dispatch({
            type: "REGISTER_FAIL",
            payload: error.response.data.msg
          });
          return error;
        };
        request.then(onSuccess, onError);
      };
    };
    
    export const register = formData => {
      const config = {
        headers: {
          "Content-Type": "application/json"
        }
      };
    
      return async dispatch => {
        const onSuccess = success => {
          dispatch({
            type: "REGISTER_SUCCESS",
            payload: success.data
          });
          return success;
        };
        const onError = error => {
          dispatch({
            type: "REGISTER_FAIL",
            payload: error.response.data.msg
          });
          return error;
        };
    
        try {
          const success = await axios.post("/api/users", formData, config);
          return onSuccess(success);
        } catch (error) {
          return onError(error);
        }
      }
    };