I was building a bulletin board app using redux-toolkit. I used the jsonPlacehlder fake API for the content of the app. But after fetching data from the API, while showing it on the UI every object is showing 2 times. The total data I am fetching from the API is 100. But due to this problem, it Shows 200 data on the UI. Every object 2 times. All the necessary codes are given below. Please help to solve this problem.
Code from 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;
Code from PostList.js component to display all the posts:
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;
There are a couple things contributing to the duplicate state:
React.StrictMode
component which applies some additional logic in non-production builds to help detect issues in your app code. In this case it is cause by Fixing bugs found by double rendering or Fixing bugs found by re-running Effects.In other words, the side-effect to fetch the posts data is being run twice and two API requests are made, the second's data appended to the first's fetched data.
To fix this you could do one or more of the following:
Update the fetchPosts.fulfilled
reducer case to replace the posts state instead of appending to it.
.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 = loadedPosts; // <-- replace posts array completely
})
Use a cancel/abort token on the fetchPosts
action so in the case the component unmounts/mounts that any in-flight API requests will be cancelled. See the Redux-Toolkit Cancellation documentation for more details.
PostsList
useEffect(() => {
const promise = dispatch(fetchPosts());
return () => {
promise?.abort();
};
}, []);
postSlice.js - Check if fetchPosts
was aborted, only set error state for non-aborted API requests.
.addCase(fetchPosts.rejected, (state, action) => {
if (action.error.message !== "Aborted") {
state.status = "failed";
state.error = action.error.message;
}
});