React-Redux Toolkit提交新数据后表格报错map不是函数不更新
问题描述
我是React与React-Redux Toolkit初学者,开发的单页应用结构非常简单:上方是地址提交表单,下方是展示已有地址的表格,需求是新数据提交到数据库后,表格能自动更新展示最新内容。首次进入页面时所有数据可以正常加载展示,但提交新数据时会抛出错误,刷新页面或者切换导航后新提交的数据又能正常显示。
报错信息如下:
Uncaught TypeError: address.address.map is not a function
正常加载时的Redux状态结构如下:
{ address: Array(1), status: 'succeeded', error: null, address: [ {id: 14, first_name: 'John', last_name: 'Doe', full_name: 'John Doe', email: 'john.doe@email.com'} ] }
相关代码
addressSlice.js
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; import axios from "axios"; const BASE_URL = 'http://127.0.0.1:8000/api/address/'; const initialState = { address: [], status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed' error: null } export const fetchAddress = createAsyncThunk('api/address', async (address, { rejectWithValue }) => { try { const response = await axios.get(BASE_URL); return response.data; } catch (err) { console.log(err); return rejectWithValue(err.response.data); } }); export const postAddress = createAsyncThunk('api/address', async (address, { rejectWithValue }) => { try { const response = await axios.post(BASE_URL, address); return response.data; } catch (err) { console.log(err); return rejectWithValue(err.response.data); } }); const addressSlice = createSlice({ name: 'address', initialState, reducers: {}, extraReducers(builder) { builder .addCase(fetchAddress.pending, (state, action) => { state.status = 'loading'; }) .addCase(fetchAddress.fulfilled, (state, action) => { state.address = action.payload; state.status = 'succeeded'; }) .addCase(fetchAddress.rejected, (state, action) => { state.status = 'failed'; state.error = action.error.message; }) } }); export const selectAllAddress = (state) => state.address; export default addressSlice.reducer
AddressCreate.js
import * as React from 'react'; import { useDispatch } from 'react-redux'; import { useState } from 'react'; import { postAddress } from '../features/addressSlice'; const AddressCreate = () => { const dispatch = useDispatch(); const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const [mail, setMail] = useState(''); const [country, setCountry] = useState(''); const [addRequestStatus, setAddRequestState] = useState('idle'); const onFirstNameChanged = (e) => setFirstName(e.target.value); const onLastNameChanged = (e) => setLastName(e.target.value); const onMailChanged = (e) => setMail(e.target.value); const onCountryChanged = (e) => setCountry(e.target.value); const canSave = [firstName, lastName, mail, country].every(Boolean) && addRequestStatus === 'idle'; const onSaveClicked = () => { if (canSave) { try { setAddRequestState('loading'); dispatch(postAddress({ first_name: firstName, last_name: lastName, email: mail, country: country, slug: '' })).unwrap(); setFirstName(''); setLastName(''); setMail(''); setCountry(''); setFirstName(''); setAddRequestState('succeeded'); } catch (err) { setAddRequestState('failed'); console.log('Failed to add new address!', err); } finally { setAddRequestState('idle'); }; }; }; return( <section> <h2>Create new address:</h2> <form> <CustomInput placeholder="First name..." value={firstName} onChange={onFirstNameChanged} /> <CustomInput placeholder="Last name..." value={lastName} onChange={onLastNameChanged} /> <CustomInput placeholder="Email..." value={mail} onChange={onMailChanged} /> <CustomInput placeholder="Country..." value={country} onChange={onCountryChanged} /> <Stack sx={{ my: "1rem" }} spacing={2} direction="row"> <Button variant="contained" onClick={onSaveClicked} disabled={!canSave} >Create address...</Button> </Stack> </form> </section> ) }; export default AddressCreate;
AddressTable.js
import * as React from 'react'; const AddressTable = ({ address }) => { return( <TableContainer sx={{ mx: 'auto', mt: '3rem', maxWidth: '75%' }} component={Paper}> <Table aria-label="simple table"> <TableHead> <TableRow> <TableCell>Name</TableCell> <TableCell align="left">Email</TableCell> <TableCell align="left">Country</TableCell> </TableRow> </TableHead> <TableBody> { address.address && address.address.map((item) => ( <TableRow key={item.id} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}> <TableCell component="th" scope="row">{item.full_name}</TableCell> <TableCell component="th" scope="row">{item.email}</TableCell> <TableCell component="th" scope="row">{item.country}</TableCell> </TableRow> )) } </TableBody> </Table> </TableContainer> ) }; export default AddressTable;
AddressList.js
import AddressTable from "../components/AddressTable"; import AddressCreate from '../components/AddressCreate'; import { useDispatch, useSelector } from 'react-redux'; import { fetchAddress, selectAllAddress, clearAdresses } from "./addressSlice"; import { useEffect } from "react"; const AddressList = () => { const dispatch = useDispatch(); const address = useSelector(selectAllAddress); useEffect(() => { dispatch(fetchAddress()); }, [dispatch]); return( <> <AddressCreate /> <AddressTable address={ address } /> </> ) }; export default AddressList;
问题原因
代码存在3个核心问题:
- 两个
createAsyncThunk的action type重名,fetchAddress和postAddress都用了'api/address'作为前缀,Redux Toolkit生成的thunk action type会冲突,postAddress.fulfilled会错误匹配到fetchAddress.fulfilled的状态处理逻辑。 - POST接口返回的是单个新创建的地址对象,不是地址数组,当
postAddress请求成功后,匹配到fetchAddress.fulfilled的处理逻辑,直接把单个对象赋值给了原本是数组类型的state.address,数组被替换成普通对象后,调用.map()自然会抛出不是函数的错误。 - 提交新地址成功后没有更新本地状态:既没有重新拉取全量地址列表,也没有手动把新地址追加到现有列表,就算不报错,表格也不会自动更新最新数据。
- 额外小问题:
AddressList.js里导入了不存在的clearAdresses方法,属于无效代码。
修复方案
- 修正thunk的action type命名,避免冲突:
// 给两个thunk设置不同的前缀 export const fetchAddress = createAsyncThunk('address/fetchAll', async (address, { rejectWithValue }) => { // 原有请求逻辑不变 }) export const postAddress = createAsyncThunk('address/addNew', async (address, { rejectWithValue }) => { // 原有请求逻辑不变 })
- 补充
postAddress的状态处理逻辑,二选一即可:
- 方案1:提交成功后重新拉取全量地址列表,逻辑简单不易出错
// 在extraReducers的builder里追加postAddress的状态处理 .addCase(postAddress.pending, (state) => { state.status = 'loading' }) .addCase(postAddress.fulfilled, (state) => { state.status = 'succeeded' }) .addCase(postAddress.rejected, (state, action) => { state.status = 'failed' state.error = action.error.message })
然后在AddressCreate.js的提交成功逻辑里,dispatch(postAddress(...))完成后,再执行一次dispatch(fetchAddress())拉取最新列表即可。
- 方案2:直接把返回的新地址追加到现有数组,不需要额外发请求
.addCase(postAddress.fulfilled, (state, action) => { state.status = 'succeeded' state.address.push(action.payload) // 新创建的地址直接加到数组末尾 })
- 增加类型兜底,避免意外报错:
修改AddressTable.js的列表渲染判断,确保只有数组类型才调用map:
{ Array.isArray(address.address) && address.address.map((item) => ( // 原有行渲染逻辑不变 )) }
- 删除
AddressList.js里无效的clearAdresses导入。
内容的提问来源于stack exchange,提问作者finethen
相关产品推荐
相关产品推荐

