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

