Redux Toolkit异步thunk触发Actions must be plain objects报错
问题根因与修复方案
你遇到的两个报错和input输入框本身没有关系,是代码逻辑错误导致的,具体问题和修复方式如下:
1. 触发Actions must be plain objects的核心原因:thunk传参格式错误+代码顺序问题
你定义fetchSearch这个异步action时,明确要求入参是包含page(数字类型)和query(字符串类型)两个字段的对象:
export const fetchSearch = createAsyncThunk('search/fetchSearch', async ({page, query} : {page: number, query: string}) => { // 内部请求逻辑 });
但你在useEffect里dispatch时,直接传入了字符串类型的query,不符合入参约定;同时你当前代码是先定义slice、后定义fetchSearch,会触发JS暂时性死区风险,slice中引用的fetchSearch可能为undefined,最终导致thunk执行链路异常,抛出action格式错误。
// 错误写法 dispatch(fetchSearch(query));
修复方式
- dispatch时传入符合类型要求的对象
- 调整模块内代码顺序,先定义createAsyncThunk,再定义slice,避免引用undefined
// 正确写法:传入包含page和query的对象 dispatch(fetchSearch({ page: 1, query: query }));
2. 触发404错误的原因:API地址配置与请求拼接错误
TMDB V3版本API的合法基础地址必须携带/3后缀,如果你配置的REACT_APP_API_BASE值为https://api.themoviedb.org(漏了末尾的/3),最终拼接的请求地址不存在,会直接返回404。
另外你当前代码把page参数硬编码为1,没有使用入参的page值,且query参数没有做URL编码,搜索关键词包含空格、中文、特殊符号时会触发请求格式错误。
修复方式
- 检查
.env文件里的REACT_APP_API_BASE配置,确保值为https://api.themoviedb.org/3 - 动态拼接page参数,用
encodeURIComponent包裹query做编码:
const response = await axios.get<ISearch<IMovie>>( `${API_BASE}/search/movie?api_key=${TMDB_API_KEY}&query=${encodeURIComponent(query)}&page=${page}&include_adult=false` );
额外必查项
如果改完上述逻辑还是报action格式错误,检查两个配置:
- 确认searchSlice生成的reducer已经正确注册到configureStore中:
import { configureStore } from '@reduxjs/toolkit'; import searchReducer from './searchSlice实际路径'; export const store = configureStore({ reducer: { search: searchReducer, // 其他业务reducer } })
- 确认组件内使用
react-redux导出的useDispatch钩子获取dispatch方法,不要直接从redux包导入原生dispatch。 - 给搜索input补上
value={query}属性,符合React受控组件规范,避免输入值和state不同步。 - 建议给输入加300ms左右的防抖,避免每输入一个字符就发一次请求,减少接口压力。
修复后参考代码
search模块完整代码
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; import axios from 'axios'; import type { ISearch, IMovie, FetchStatus } from './你的类型定义路径'; const API_BASE = process.env.REACT_APP_API_BASE; const TMDB_API_KEY = process.env.REACT_APP_API_KEY; // 异步action放在slice前定义,避免引用问题 export const fetchSearch = createAsyncThunk('search/fetchSearch', async ({page, query} : {page: number, query: string}) => { const response = await axios.get<ISearch<IMovie>>( `${API_BASE}/search/movie?api_key=${TMDB_API_KEY}&query=${encodeURIComponent(query)}&page=${page}&include_adult=false` ); return response.data; }); interface ISearchState { searchQuery: string; searchPage: number; searchResult: ISearch<IMovie> | null; fetchStatus: FetchStatus | null; } const initialState: ISearchState = { searchQuery: '', searchPage: 1, searchResult: null, fetchStatus: null, }; const searchSlice = createSlice({ name:'search', initialState, reducers: { setSearchQuery: (state, action) => { state.searchQuery = action.payload; }, setSearchPage: (state, action) => { state.searchPage = action.payload; } }, extraReducers(builder) { builder .addCase(fetchSearch.pending, (state) => { state.fetchStatus = FetchStatus.PENDING; }) .addCase(fetchSearch.fulfilled, (state, action) => { state.fetchStatus = FetchStatus.SUCCESS; state.searchResult = action.payload; }) .addCase(fetchSearch.rejected, (state) => { state.fetchStatus = FetchStatus.FAILURE; }) } }) export const { setSearchQuery, setSearchPage } = searchSlice.actions; export default searchSlice.reducer;
搜索栏组件代码
import { useState, useEffect } from 'react'; import { useDispatch } from 'react-redux'; import { fetchSearch } from './searchSlice实际路径'; const SearchBar = () => { const [query, setQuery] = useState(""); const dispatch = useDispatch(); useEffect(() =>{ if(query.length > 0){ dispatch(fetchSearch({ page: 1, query })); } }, [query, dispatch]) return ( <form onSubmit={e => e.preventDefault()}> <input type="search" placeholder="Search for a movie..." autoFocus value={query} onChange={event => setQuery(event.target.value)} /> </form> ) } export default SearchBar;
内容的提问来源于stack exchange,提问作者Carl
相关产品推荐
相关产品推荐

