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

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个核心问题:

  1. 两个createAsyncThunk的action type重名,fetchAddress和postAddress都用了'api/address'作为前缀,Redux Toolkit生成的thunk action type会冲突,postAddress.fulfilled会错误匹配到fetchAddress.fulfilled的状态处理逻辑。
  2. POST接口返回的是单个新创建的地址对象,不是地址数组,当postAddress请求成功后,匹配到fetchAddress.fulfilled的处理逻辑,直接把单个对象赋值给了原本是数组类型的state.address,数组被替换成普通对象后,调用.map()自然会抛出不是函数的错误。
  3. 提交新地址成功后没有更新本地状态:既没有重新拉取全量地址列表,也没有手动把新地址追加到现有列表,就算不报错,表格也不会自动更新最新数据。
  4. 额外小问题:AddressList.js里导入了不存在的clearAdresses方法,属于无效代码。

修复方案
  1. 修正thunk的action type命名,避免冲突:
// 给两个thunk设置不同的前缀
export const fetchAddress = createAsyncThunk('address/fetchAll', async (address, { rejectWithValue }) => {
  // 原有请求逻辑不变
})
export const postAddress = createAsyncThunk('address/addNew', async (address, { rejectWithValue }) => {
  // 原有请求逻辑不变
})
  1. 补充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) // 新创建的地址直接加到数组末尾
})
  1. 增加类型兜底,避免意外报错:
    修改AddressTable.js的列表渲染判断,确保只有数组类型才调用map:
{ Array.isArray(address.address) && address.address.map((item) => (
  // 原有行渲染逻辑不变
)) }
  1. 删除AddressList.js里无效的clearAdresses导入。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.03 06:31:27