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
相关产品推荐
相关产品推荐

