skip to Main Content

When trying to add product to user favorites I am getting this error. Although the state of redux is getting updated properly.
I have used Redux Thunk for Authentication, but I was trying to avoid adding to many extra reducers as the add a lot of code just for one function; and start using productService.js file.

userSlice.js

import {
  createAction,
  createSlice,
} from "@reduxjs/toolkit";
import {
  loginUser,
  registerUser,
} from "../actions/authActions";
import { getWithExpiry } from "../../utils/setStorage";

export const addFavoritesError = createAction(
  "addFavoritesError"
);

const savedUserInfo = getWithExpiry("userInfo")
  ? getWithExpiry("userInfo")
  : null;
const savedToken = getWithExpiry("userToken")
  ? getWithExpiry("userToken")
  : null;

const initialValue = {
  isLoading: false,
  userInfo: savedUserInfo,
  userToken: savedToken,
  error: null,
  success: false,
};

const authSlice = createSlice({
  name: "auth",
  initialState: initialValue,
  reducers: {
    login: (state) => {
      loginUser().then((res) => {
        // history.pushState('/');
      });
    },

    logOut: (state) => {
      return {
        ...state.initialState,
        isLoading: false,
        userInfo: null,
        userToken: null,
        error: null,
        success: false,
      };
    },
    signUp: (state) => {
      registerUser().then((res) => {
        // history.pushState('/');
      });
    },

    addFavorites: (state, action) => {
      return {
        ...state,
        userInfo: {
          ...state.userInfo,
          favorites: [...action.payload],
        },
      };
    },
  },
  extraReducers: {
    // 1 login user
    [loginUser.pending]: (state) => {
      state.isLoading = true;
      state.error = null;
    },

    [loginUser.fulfilled]: (state, { payload }) => {
      state.isLoading = false;
      state.success = true;
      state.userInfo = payload.data.user;
      state.userToken = payload.token;
    },

    [loginUser.rejected]: (state, { payload }) => {
      state.isLoading = false;
      state.error = payload;
    },

    // 2 register user

    [registerUser.pending]: (state) => {
      state.isLoading = true;
      state.error = null;
    },

    [registerUser.fulfilled]: (state, { payload }) => {
      state.isLoading = false;
      state.success = true;
      state.userInfo = payload;
      state.userToken = payload.token;
    },

    [registerUser.rejected]: (state, { payload }) => {
      state.isLoading = false;
      state.error = payload;
    },
  },
});

export const { logOut, addFavorites } = authSlice.actions;

export default authSlice.reducer

userService.js

import apiActions from "../../utils/api";
import { addFavorites, addFavoritesError } from "../slices/userSlice";

export const AddProdToFavorites = async (dispatch, user_id, prod_id) => {
  try {
    // api call
    const data = await apiActions.addProdToUserFav(user_id, prod_id);
    dispatch(addFavorites(data));
  } catch {
    dispatch(addFavoritesError());
  }
};`

ProductCard.js

import React, { useCallback, useState } from "react";
import { useDispatch} from "react-redux";
import { AddProdToFavorites} from "../../../store/services/userService";


function DestinationsDisplayCard(props) {
  const { userToken, name, price} = props;
  const [favored, setFavored] = useState(false);

  const dispatch = useDispatch();
  const addItemtoFav = useCallback(() => {
    setFavored((currrent) => !currrent);
    dispatch(AddProdToFavorites(dispatch, userToken, prodId));
  }, [favored]);

const removeItemFromFav = useCallback(() => {}, [favored]);

return (
  <div className="productCard">
        <p>{name}</p>
        <p>${price}</p>
        {userToken ? (
          <button
            onClick={favored ? removeItemFromFav : addItemtoFav}
          >
          </button>
        ) : null}
      </div>

)
}

api.js

import axios from "axios";
const baseURL = "<<the url for api>>";

const apiActions = {
addProdToUserFav: async (user_id, prod_id) => {
    const res = await axios.post(
      `${baseURL}/v1/user/addFavorite`,
      {
        userId: user_id,
        prodId: prod_id,
      }
    );
    return res.data.data.user.favorites;
  },
// this endpoint returns a list of object [{product2}{product2}] (with name, price, discount, etc)
}

I expected that state.userInfo.favorites to update, which it does, but I am getting this error.

Uncaught Error: Actions must be plain objects. Use custom middleware for async actions.
VM41:1 Uncaught Error: Actions must be plain objects. Use custom middleware for async actions.
    at Object.performAction (<anonymous>:1:41504)
    at k (<anonymous>:3:1392)
    at s (<anonymous>:3:5102)
    at serializableStateInvariantMiddleware.ts:210:1
    at index.js:20:1
    at Object.dispatch (immutableStateInvariantMiddleware.ts:264:1)
    at dispatch (<anonymous>:6:7391)
    at DestinationsDisplay.card.jsx:23:1
    at HTMLUnknownElement.callCallback (react-dom.development.js:4164:1)
    at Object.invokeGuardedCallbackDev (react-dom.development.js:4213:1)

2

Answers


  1. My prior comment implied that Redux Thunk had to be explicitly introduced, which was wrong. If you’re using Redux Toolkit and still getting the "Actions must be plain objects" error, there might be another problem in your code. Please double-check that you are doing the actions correctly. Make sure your Redux slice file (userSlice.js) calls the Redux Toolkit’s createSlice method. Make sure you’re dispatching the actions directly in your async thunk action (AddProdToFavorites). Here’s how your AddProdToFavorites async thunk should appear within your Redux Toolkit slice (userSlice.js):

    import { createSlice } from "@reduxjs/toolkit";
    import apiActions from "../../utils/api";
    
    const userSlice = createSlice({
      name: "user",
      initialState: { /* Your initial state here */ },
      reducers: {
        addFavorites: (state, action) => {
          state.userInfo.favorites = action.payload;
        },
        addFavoritesError: (state) => {
          // Handle the error state if needed
        },
      },
    });
    
    // Async thunk action
    export const AddProdToFavorites = (user_id, prod_id) => async (dispatch) => {
      try {
        const data = await apiActions.addProdToUserFav(user_id, prod_id);
        dispatch(addFavorites(data));
      } catch (error) {
        dispatch(addFavoritesError());
        // Handle the error as needed
      }
    };
    
    export const { addFavorites, addFavoritesError } = userSlice.actions;
    export default userSlice.reducer;
    

    Please ensure that your Redux slice, actions, and reducers are properly configured.

    Login or Signup to reply.
  2. You have a few issues with the way you are trying to use regular reducer functions to handle asynchronous side-effects, and dispatching objects to the store that are neither action objects nor asynchronous action creators, e.g. Thunks.

    AddProdToFavorites is a regular function and should be rewritten to use Redux-Toolkit’s createAsyncThunk.

    The suggested/recommended patter is to return directly the fetched data or rejected Promise with error value, and use the state slice’s extraReducers to handle the state updates. Remember that with RTK you can write mutable state updates, you don’t need to shallow copy all the state.

    import { createAsyncThunk } from "@reduxjs/toolkit";
    import apiActions from "../../utils/api";
    
    export const AddProdToFavorites = createAsyncThunk(
      "addProdToFavorites",
      async ({ user_id, prod_id }, thunkApi) => {
        try {
          // api call
          const data = await apiActions.addProdToUserFav(user_id, prod_id);
          return data;
        } catch(error) {
          return thunkApi.rejectWithValue(error);
        }
      },
    );
    
    const authSlice = createSlice({
      name: "auth",
      initialState: initialValue,
      reducers: {
        ...
        addFavorites: (state, action) => {
          state.userInfo.favorites = action.payload;
        },
      },
      extraReducers: builder => {
        ...
      },
    });
    

    or if you are really wanting to write and keep the extra actions, use the second argument to the thunk to access the dispatch function and dispatch your other actions to the store. Handle these actions in the regular reducers with the same mutable state update.

    import { createAsyncThunk } from "@reduxjs/toolkit";
    import apiActions from "../../utils/api";
    import { addFavorites, addFavoritesError } from "../slices/userSlice";
    
    export const AddProdToFavorites = createAsyncThunk(
      "addProdToFavorites",
      async ({ user_id, prod_id }, thunkApi) => {
        try {
          // api call
          const data = await apiActions.addProdToUserFav(user_id, prod_id);
          thunkApi.dispatch(addFavorites(data));
        } catch(error) {
          thunkApi.dispatch(addFavoritesError(error));
        }
      },
    );
    
    const authSlice = createSlice({
      name: "auth",
      initialState: initialValue,
      reducers: {
        ...
      },
      extraReducers: builder => {
        builder
          .addCase(AddProdToFavorites.fulfilled, (state, action) => {
            state.userInfo.favorites = action.payload;
          })
          ...
      },
    });
    

    Dispatching AddProdToFavorites action:

    function DestinationsDisplayCard({ userToken, name, price }) {
      const dispatch = useDispatch();
      
      const [favored, setFavored] = useState(false);
    
      const addItemtoFav = () => {
        setFavored((favored) => !favored);
        dispatch(AddProdToFavorites({ userToken, prodId }));
      };
    
      const removeItemFromFav = () => {};
    
      return (
        <div className="productCard">
          <p>{name}</p>
          <p>${price}</p>
          {userToken && (
            <button
              onClick={favored ? removeItemFromFav : addItemtoFav}
            >
              ...
            </button>
          }
        </div>
      );
    }
    

    In authSlice the login and signup actions/reducers are invalid. Reducer functions are to be pure, synchronous functions. These two actions should also be Thunks, and the UI should handle chaining or await-ing them to settle in order to issue the additional side-effect, e.g. the navigation. It looks like loginUser and registerUser are already RTK Thunks, so remove the login and signup actions/reducers from the authSlice and handle the asynchronous logic in the calling function.

    Example:

    const authSlice = createSlice({
      name: "auth",
      initialState: initialValue,
      reducers: {
        logOut: (state) => {
          return {
            ...state.initialState,
            isLoading: false,
            userInfo: null,
            userToken: null,
            error: null,
            success: false,
          };
        },
      },
      extraReducers: builder => {
        builder
          .addCase(AddProdToFavorites.fulfilled, (state, action) => {
            state.userInfo.favorites = action.payload;
          })
          .addCase(loginUser.pending, (state) => {
            state.isLoading = true;
            state.error = null;
          })
          .addCase(loginUser.fulfilled, (state, { payload }) => {
            state.isLoading = false;
            state.success = true;
            state.userInfo = payload.data.user;
            state.userToken = payload.token;
          })
          .addCase(loginUser.rejected, (state, { payload }) => {
            state.isLoading = false;
            state.error = payload;
          })
          .addCase(registerUser.pending, (state) => {
            state.isLoading = true;
            state.error = null;
          })
          .addCase(registerUser.fulfilled, (state, { payload }) => {
            state.isLoading = false;
            state.success = true;
            state.userInfo = payload;
            state.userToken = payload.token;
          })
          .addCase(registerUser.rejected, (state, { payload }) => {
            state.isLoading = false;
            state.error = payload;
          });
      },
    });
    

    Unwrap the returned resolved Promise to handle the fulfilled/rejected status.

    const SomeComponent = () => {
      const dispatch = useDispatch();
      const history = useHistory();
    
      const loginHandler = async () => {
        try {
          await dispatch(loginUser()).unwrap();
          history.push("/");
        } catch(error) {
          // handle login errors, etc...
        }
      };
    
      ...
    };
    

    or

    const SomeComponent = () => {
      const dispatch = useDispatch();
      const history = useHistory();
    
      const loginHandler = () => {
        dispatch(loginUser()).unwrap()
          .then(() => {
            history.push("/");
          })
          .catch((error) => {
            // handle login errors, etc...
          });
      };
    
      ...
    };
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search