2

I am trying to make a simple blog webpage with react and redux(going along a tutorial). I created a redux store, made a posts slice that is used to manage everything that has to do with the posts and i am having some issues with fetching the data using redux thunk. When i run the blog page i see two of every post. Not sure if the issue is from the component i used to dispatch the fetchPosts() function and display the posts or some error i made in the slice itself. Below is my code and a jsFiddle with my project. I'd really appreciate it if someone can help me check where i went wrong.

PostsList.js

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

  const posts = useSelector(selectAllPosts);
  const postStatus = useSelector(getPostsStatus);
  const error = useSelector(getPostsError);

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

  let content;
  if (postStatus === "loading") {
    content = <p>"Loading..."</p>;
  } else if (postStatus === "succeeded") {
    const orderedPosts = posts
      .slice()
      .sort((a, b) => b.date.localeCompare(a.date));
    content = orderedPosts.map((post) => (
      <PostsExcerpt key={post.id} post={post} />
    ));
  } else if (postStatus === "failed") {
    content = <p>{error}</p>;
  }

  return (
    <section>
      <h2>Posts</h2>
      {content}
    </section>
  );
};
export default PostsList;

PostsSlice.js

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

const POSTS_URL = "https://jsonplaceholder.typicode.com/posts";

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

//Fetch Posts Function
export const fetchPosts = createAsyncThunk("posts/fetchPosts", async () => {
  const response = await axios.get(POSTS_URL);
  return response.data;
});

export const addNewPost = createAsyncThunk(
  "posts/addNewPost",
  async (initialPost) => {
    const response = await axios.post(POSTS_URL, initialPost);
    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: {
              thumbsUp: 0,
              wow: 0,
              heart: 0,
              rocket: 0,
              coffee: 0,
            },
          },
        };
      },
    },
    reactionAdded(state, action) {
      const { postId, reaction } = action.payload;
      const existingPost = state.posts.find((post) => post.id === postId);
      if (existingPost) {
        existingPost.reactions[reaction]++;
      }
    },
  },

  extraReducers(builder) {
    builder
      .addCase(fetchPosts.pending, (state) => {
        state.status = "loading";
      })
      .addCase(fetchPosts.fulfilled, (state, action) => {
        state.status = "succeeded";
        // Adding date and reactions
        let min = 1;
        const loadedPosts = action.payload.map((post) => {
          post.date = sub(new Date(), { minutes: min++ }).toISOString();
          post.reactions = {
            thumbsUp: 0,
            wow: 0,
            heart: 0,
            rocket: 0,
            coffee: 0,
          };
          return post;
        });

        // Add any fetched posts to the array
        state.posts = state.posts.concat(loadedPosts);
      })
      .addCase(fetchPosts.rejected, (state, action) => {
        state.status = "failed";
        state.error = action.error.message;
      })
      .addCase(addNewPost.fulfilled, (state, action) => {
        // Fix for API post IDs:
        // Creating sortedPosts & assigning the id
        // would be not be needed if the fake API
        // returned accurate new post IDs
        const sortedPosts = state.posts.sort((a, b) => {
          if (a.id > b.id) return 1;
          if (a.id < b.id) return -1;
          return 0;
        });
        action.payload.id = sortedPosts[sortedPosts.length - 1].id + 1;
        // End fix for fake API post IDs

        action.payload.userId = Number(action.payload.userId);
        action.payload.date = new Date().toISOString();
        action.payload.reactions = {
          thumbsUp: 0,
          hooray: 0,
          heart: 0,
          rocket: 0,
          eyes: 0,
        };
        console.log(action.payload);
        state.posts.push(action.payload);
      });
  },
});

export const selectAllPosts = (state) => state.posts.posts;
export const getPostsStatus = (state) => state.posts.status;
export const getPostsError = (state) => state.posts.error;

export const { postAdded, reactionAdded } = postsSlice.actions;

export default postsSlice.reducer;

This is the outcome
enter image description here

Incase nothing i suspet to be wrong is the issue This is a github repo with my code. Thank you

I tried making changes to the slice, the fetchPosts function and the PostsList component but still no outcome.

ksav
  • 20,015
  • 6
  • 46
  • 66
Destiny Kefas
  • 35
  • 1
  • 6
  • Why are you doing this `state.posts = state.posts.concat(loadedPosts)`? – ksav Jan 23 '23 at 13:22
  • @ksav you mean why directly mutating? if it's that then it's because the tutorial i was using said you are allowed to directly mutate state when you are inside a createSlice, because of emmer js – Destiny Kefas Jan 23 '23 at 13:33
  • Yes i understand that RTK uses Immer. But why are you concatenating in the first place? – ksav Jan 23 '23 at 13:35
  • In general that's the code from the tutorial, i actually just followed the instructions, but the fact that it isnt working makes me feel i made a mistake somewhere. I've spent over a day trying to find my error and can't so that's why i'm asking. – Destiny Kefas Jan 23 '23 at 13:37
  • 1) The tutorial author may not be using React 18. 2) This Issue won't present itself in production. – ksav Jan 23 '23 at 13:38
  • That said, there's probably no need to concatenate unless there's some kind of infinite pagination happening. – ksav Jan 23 '23 at 13:40
  • 1
    yeah i got rid of concatenation and just assigned the posts state to the loadedPost. Thank you, your comment was instrumental – Destiny Kefas Jan 23 '23 at 13:43

1 Answers1

0

The problem is that useEffect will be called twice on mount since React 18 when you are in development under Strict Mode.

In PostsList, this means that you may not realise that you are dispatching fetchPosts twice. e.g.
dispatch(fetchPosts())

We can see this happening in the Redux devtools.

enter image description here

Then in PostSlice you are concatenating the results of the second fetchPosts thunk to the end of the results of the first fetchPost thunk. e.g.

state.posts = state.posts.concat(loadedPosts)

Instead, change this line to:
state.posts = loadedPosts;

ksav
  • 20,015
  • 6
  • 46
  • 66
  • 1
    Thank you very much. I knew it would be something related to useEffect acting different due to version changes. This worked thank you very much. – Destiny Kefas Jan 23 '23 at 13:42