Search code examples
reactjsreduxinfinite-scrollpayload

Infinite scrolling with redux


I am faced with this problem.

I have a call in my backend which returns me an array of elements depending on the requested page, for simplicity let's say it is structured like this

export const fetchPosts = (page) => API.get(`/posts?page=${page}`)

Now in my action, when the page loads I simply call my endpoint (default page to 1) and get my posts. In this way :

export const getPosts = (page) => async (dispatch) => {
  try {
    
    dispatch({
      type: START_LOADING,
    });
    const { data } = await api.fetchPosts(
      page,
      store.getState().auth.user?._id
    );
      
    dispatch({
      type: GET_POSTS,
      payload: data,
    });
    dispatch({
      type: END_LOADING,
    });
  } catch (error) {
    console.log(error);
  }
};

and finally my reducer which simply assigns the value to my initial posts state like so:

const initialState = {
  posts: [],
  currentPage: null,
  numberOfPages: null,
  isLoading: true,
  post: {},
  postsOfCurrentUser: [],
};



case GET_POSTS:
      return {
        posts: payload.data,
        currentPage: payload.currentPage,
        numberOfPages: payload.numberOfPages,
        post: {},
      };

Now, what I would like to try to do is that when you press a button or scroll a new call is made to my backend passing the new page and so far no problem. The problem arises instead when I would like to modify the past payload which for simplicity we assume to be this : posts on initial page load:

[{title : 1} , {title:2} , {title:3}]

after pressing the button or scrolling I would like it to be:

[{title : 1} , {title:2} , {title:3},{title : 4} , {title:5} , {title:6}]

so i tried like this in my action :

export const getPosts = (page) => async (dispatch) => {
  try {
    dispatch({
      type: START_LOADING,
    });
    const { data } = await api.fetchPosts(
      page,
      store.getState().auth.user?._id
    );
    if (page === 1) {
      dispatch({
        type: GET_POSTS,
        payload: data,
      });
    } else {
      dispatch({
        type: GET_POSTS,
        payload: [...store.getState().posts.posts, data],
      });
    }

    dispatch({
      type: END_LOADING,
    });
  } catch (error) {
    console.log(error);
  }
};

but what i get is this:

[
   [
      {title : 1} , {title:2} , {title:3}
   ]
   ,{title : 4} , {title:5} , {title:6}
]

and obviously everything explodes. How can I do this? Are there better ways? Thanks in advance to who will answer me .


Solution

  • Your GET_POSTS action does not need to contain the entire array of posts, including those which are already in the state. You can dispatch an action which contains the array of posts for the page and the current page number. The if (page === 1) logic is something that would be better to handle in the reducer.

    I was going to suggest that you dispatch an action where the payload is an object with properties posts and page. But looking at your reducer, it seems like the data variable from your API already contains that information in the .currentPage property. So you can just delete all of the extra logic that you added to your action creator and go back to the first version that you posted, where all pages dispatch the same action:

    dispatch({
      type: GET_POSTS,
      payload: data
    });
    

    (You can make your thunk action much cleaner and simpler with the createAsyncThunk function from Redux Toolkit.)

    It's the reducer's job to figure out what to do based on the page number.

    case GET_POSTS:
      const { data, currentPage, numberOfPages } = payload;
      return {
         ...state,
         posts: currentPage === 1 ? data : [...state.posts, ...data]
         currentPage,
         numberOfPages
      };
    

    I am using a ternary operator to either replace the entire posts array or add to the existing array, depending on the value of currentPage.