You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Redux报错:Actions必须是纯对象,添加商品到收藏夹时触发

问题描述

尝试将商品添加到用户收藏夹时,触发错误:Actions must be plain objects. Use custom middleware for async actions.。尽管Redux状态已经正常更新,但这个错误始终存在。我在认证功能中已经使用了Redux Thunk,为了避免添加过多额外reducer,尝试通过productService.js实现收藏功能,相关代码如下:

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>

)
}

authActions.js

import { createAsyncThunk } from "@reduxjs/toolkit";
import apiActions from "../../utils/api";
import { setWithExpiry } from "../../utils/setStorage";

export const loginUser = createAsyncThunk(
  "auth/login",
  async ({ email, password }, { rejectWithValue }) => {
    try {
      const data = await apiActions.loginUser(
        email,
        password
      );

      setWithExpiry("userToken", data.token, 900000);
      setWithExpiry("userInfo", data.data.user, 900000);

      return data;
    } catch (error) {
      if (error.response && error.response.data.message) {
        return rejectWithValue(error.response.data.message);
      } else {
        return rejectWithValue(error.message);
      }
    }
  }
);

export const registerUser = createAsyncThunk(
  "auth/register",
  async (
    { name, email, password },
    { rejectWithValue }
  ) => {
    console.log("thunk - register", name, email, password);
    try {
      const data = await apiActions.registerUser(
        name,
        email,
        password
      );
      localStorage.setItem("userToken", data.token);

      return data;
    } catch (error) {
      if (error.response && error.response.data.message) {
        return rejectWithValue(error.response.data.message);
      } else {
        return rejectWithValue(error.message);
      }
    }
  }
);

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

错误栈信息

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)

解决方案

问题根源

你直接把异步函数AddProdToFavorites的执行结果(Promise对象)传给了dispatch,但Redux的dispatch仅支持接收普通对象类型的action,或由Redux Thunk处理的返回函数的异步action。当前写法不符合Thunk规范,因此触发错误。同时代码还有几处小问题需要修正。

具体修改步骤

1. 重构userService.js为标准Thunk函数

将AddProdToFavorites改为返回函数的形式,符合Redux Thunk要求:

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

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());
    console.error('添加收藏失败:', error);
  }
};

2. 修改ProductCard.js中的调用逻辑

直接将Thunk函数传给dispatch,无需手动传递dispatch参数,同时补充缺失的prodId:

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

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

  const dispatch = useDispatch();
  const addItemtoFav = useCallback(() => {
    setFavored((current) => !current);
    dispatch(addProdToFavorites(userToken, prodId));
  }, [dispatch, userToken, prodId]);

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

  return (
    <div className="productCard">
          <p>{name}</p>
          <p>${price}</p>
          {userToken ? (
            <button
              onClick={favored ? removeItemFromFav : addItemtoFav}
            >
              {favored ? '取消收藏' : '添加收藏'}
            </button>
          ) : null}
        </div>
  )
}

3. 修复userSlice.js中的错误用法

login和signUp reducer中直接调用thunk函数的写法错误,应删除这两个reducer,在组件中通过dispatch触发对应的thunk:

const authSlice = createSlice({
  name: "auth",
  initialState: initialValue,
  reducers: {
    logOut: (state) => {
      return {
        ...state.initialState,
        isLoading: false,
        userInfo: null,
        userToken: null,
        error: null,
        success: false,
      };
    },
    addFavorites: (state, action) => {
      return {
        ...state,
        userInfo: {
          ...state.userInfo,
          favorites: [...action.payload],
        },
      };
    },
  },
  // 其余代码不变
});

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

4. 可选:简化addFavorites reducer写法

Redux Toolkit允许直接修改状态,无需返回新对象:

addFavorites: (state, action) => {
  state.userInfo.favorites = [...action.payload];
},

原理说明

Redux Thunk的核心是让dispatch可以接收一个函数,这个函数会被Thunk中间件调用并传入dispatch参数,从而在函数内部执行异步操作,完成后再分发普通对象类型的action。之前的写法是直接传递Promise给dispatch,不符合Thunk的工作机制,因此触发错误。

内容的提问来源于stack exchange,提问作者John Vasile

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 22:34:57