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

React Redux-Toolkit列表重复渲染:API数据在UI中显示两次

问题:Redux-Toolkit获取数据后UI重复显示每条内容

使用Redux-Toolkit开发公告板应用,通过jsonPlaceholder模拟API获取数据,但UI中每条数据都重复显示,原本100条数据变成200条。

相关代码

postSlice.js

import { createSlice, nanoid,createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";
import { sub } from "date-fns";

const POSTS_URL = 'http://jsonplaceholder.typicode.com/posts';

const initialState = {
    posts: [],
    status: 'idle',
    error: null
}

export const fetchPosts = createAsyncThunk('posts/getPosts', async () => {
    const response = await axios.get(POSTS_URL);
    // console.log(response.data)
    return response.data;
})

const postsSlice = createSlice({
    name: 'posts',
    initialState,
    reducers: {
        postAdded: {
            reducer(state, action) {
                state.posts.push(action.payload)
            },
            prepare(title, content, userId) {
                return{
                    payload: {
                        id: nanoid(),
                        title,
                        content,
                        date: new Date().toISOString(),
                        userId,
                        reactions: {
                            like: 0,
                            love: 0,
                            wow: 0,
                            coffee: 0
                        }
                    }
                }
            }
        },
        addReactions(state, action) {
            const { postId, reaction } = action.payload;
            const postToReact = state.posts.find(post => post.id === postId);
            if(postToReact){
                postToReact.reactions[reaction]++
            }
        }
    },
    extraReducers(builder) {
        builder
            .addCase(fetchPosts.pending, (state, action) => {
                    state.status = 'loading'
            })
            .addCase(fetchPosts.fulfilled, (state, action) => {
                state.status = 'succeeded';

                // adding date and reactions because they are not available in the api data
                let min = 1;
                const loadedPosts = action.payload.map(post => {
                    post.date = sub(new Date(), {minutes: min++}).toISOString();
                    post.reactions = {
                            like: 0,
                            love: 0,
                            wow: 0,
                            coffee: 0
                        }
                    return post;
                })
                state.posts = state.posts.concat(loadedPosts);
            })
            .addCase(fetchPosts.rejected, (state, action) => {
                state.status = 'failed';
                state.error = action.error.message
            })
    }
});

export const selectAllPost = state => state.posts.posts;
export const getPostStatus = state => state.posts.status;
export const getPostError = state => state.posts.error;

export const { postAdded, addReactions } = postsSlice.actions

export default postsSlice.reducer;

PostList.js

import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import Post from './Post';
import { selectAllPost, getPostStatus, getPostError, fetchPosts } from '../../features/posts/postsSlice';
import { parseISO } from 'date-fns';

const PostsList = () => {
    const dispatch = useDispatch()

    const posts = useSelector(selectAllPost);
    const postStatus = useSelector(getPostStatus);
    const postError = useSelector(getPostError);

    useEffect(() => {
        if (postStatus === 'idle') {
            dispatch(fetchPosts())
        }
    }, [postStatus, dispatch])

    let content;
    if(postStatus === 'loading'){
        content = <span className="loading loading-bars loading-lg"></span>
    } else if(postStatus === 'succeeded') {
        const sortedPosts = posts.slice().sort((a, b) => parseISO(b.date) - parseISO(a.date));
        content = sortedPosts.map((post, index) => 
            <Post key={index} post={post} />
        );
        console.log(sortedPosts)
    } else if(postStatus === 'failed') {
        content = {postError}
    }

    return (
        <div>
            <h1 className='text-center text-2xl font-bold mb-4'>Posts</h1>
            {content}
        </div>
    )
}

export default PostsList;

解决方法

1. 替换数据而非追加

在postSlice.js的fetchPosts.fulfilled处理逻辑中,将state.posts = state.posts.concat(loadedPosts);改为直接赋值:

state.posts = loadedPosts;

原代码使用concat会把新获取的数据追加到现有数组末尾,如果fetchPosts被触发多次(比如React Strict Mode开发环境下的重复渲染、组件重复挂载等),就会导致数据重复。直接赋值会覆盖原有数组,确保每次获取数据后都是最新的完整列表。

2. 修正列表项的key值

在PostList.js中,将map函数的key从index改为post.id:

content = sortedPosts.map(post => 
    <Post key={post.id} post={post} />
);

使用数组索引作为key是不良实践,当列表内容变化时可能导致React渲染异常。使用数据本身的唯一ID(post.id)能让React准确识别每个列表项,避免不必要的渲染问题。

3. 检查重复触发请求的原因

如果开发环境下使用了React Strict Mode,它会在开发阶段触发两次组件挂载,导致useEffect执行两次。虽然上述直接赋值的方法已经能避免数据重复,但也可以通过添加额外的状态标记来防止重复请求,比如在initialState中增加isFetching状态,确保同一时间只有一个请求在进行。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 11:53:10