react-infinite-scroller icon indicating copy to clipboard operation
react-infinite-scroller copied to clipboard

Having troubles using custom state management, loadMore called multiple times?

Open gremo opened this issue 4 years ago • 7 comments

I'm using a custom state management instead of using the page parameter for loading items. This because I need to "reset" the page after some search parameters changes.

Basically this is what I'm storing:

const [pagination, setPagination] = useState({ items: [], hasMore: true, loading: true, error: false, nextPage: 1 });

... and this is my loadMore function:

  const loadMore = () => {
    const fetch = async () => {
      try {
        // Fetch the items passing the "page" parameter from the state
        const { items, headers } = await axios.get(apiUrl, { params: { ...params, page: pagination.nextPage } })
          .then(({ data, headers }) => ({ items: data, headers }));

        setPagination(prev => ({ ...prev,
          items: [...prev.items, ...items], // Append new items to the existing ones (this is how to component works)
          hasMore: pagination.nextPage < parseInt(headers['x-total-pages']), // X-Total-Pages comes from the server
          nextPage: pagination.nextPage + 1,
        }));
      } catch (error) {
        setPagination(prev => ({ ...prev, error: false }));
      } finally {
        setPagination(prev => ({ ...prev, loading: false }));
      }
    };

    fetch();
  };

That is, after a successfully fetch, update the nextPage parameter and hasMore.

What is happening is strange: on the first page load I have multiple calls with the very same nextPage value:

Cattura

The component isn't very complex or special:

  <div style={{ height: '700px', overflow: 'auto' }} ref={scrollContainer}>
    <InfiniteScroll
      pageStart={0}
      loadMore={loadMore}
      hasMore={pagination.hasMore}
      useWindow={false}
      getScrollParent={() => scrollContainer.current}
      loader={
        <div key={0} className="flex flex-col items-center content-center w-full p-3">
          <HalfCircleSpinner color="red"/>
        </div>
      }>
      {pagination.items.map(item => <p key={item.id}>#{item.id} {item.name}</p>)}
    </InfiniteScroll>
  </div>

gremo avatar Sep 13 '20 12:09 gremo

I used another variable call "isAPICall". then I set it to true before my API call and then set again it to false in end of the api call. before start the api call i check that variable is false.

const [isAPICall, setStartAPICall] = useState(false);

let loadData=()=>{

        if(!hasMore || isAPICall) return;
        setStartAPICall(true);

        get(`url`).then((response: any) => {
            if (response.data.statusCode === 200) {
                if (response.data.result != null) {
                    setPendingCampaignList(prevPendingCampaignList=> [...prevPendingCampaignList, ...response.data.result]);
                    setOffset(prevOffset => prevOffset + limit);
                    setHasMore(response.data.result.length === limit);
                } else {
                    let messageBox = {
                        show: true,
                        title: "Error",
                        className: "error",
                        content: "Error While Getting the Data",
                        isConfirmation: false,
                        callBackFunction: null
                    }
                    dispatch(showMessageBox(messageBox));
                }
            } else {
                let errorType = response.data.errorList[0].errorType;
                let messageBox = {
                    show: true,
                    title: ErrorTypes.Error === errorType ? "Error" : "Warning",
                    className: ErrorTypes.Error === errorType ? "error" : "warning",
                    content: response.data.errorList[0].statusMessage,
                    isConfirmation: false,
                    callBackFunction: null
                }
                dispatch(showMessageBox(messageBox));
            }
        setStartAPICall(false);

        });
    
    }

Surangaup avatar Sep 20 '20 04:09 Surangaup

Very true. I faced a similar problem when implementing InfiniteScroll with Apollo v3 pagination. I ended up just using the loading from Apollo, as well as by checking the network status. I guess the trigger for loadMore doesn't really have any idea if the previous request is still active.

EDIT: I probably should have been more clear on my solution. For me, I had to keep track of the loading state manually and only load more if loading is false. See the loadMoreProducts function below.

import { gql, useQuery, NetworkStatus } from '@apollo/client'
import InfiniteScroll from 'react-infinite-scroller'

export const ALL_PRODUCTS_QUERY = gql`
  query products($start: Int!, $limit: Int!) {
    products(start: $start, limit: $limit) {
      id
      name
    }
    productsCount
  }
`

export const productsQueryVars = {
  start: 0,
  limit: 12
}

export default function Products() {
  const { loading, error, data, fetchMore, networkStatus } = useQuery(
    ALL_PRODUCTS_QUERY,
    {
      variables: productsQueryVars,
      // Setting this value to true will make the component rerender when
      // the "networkStatus" changes, so we are able to know if it is fetching
      // more data
      notifyOnNetworkStatusChange: true
    }
  )

  const loadingMoreProducts = networkStatus === NetworkStatus.fetchMore

  if (error) return <div>Error</div>
  if (loading && !loadingMoreProducts) return <div>Loading</div>

  const { products, productsCount } = data

  const areMoreProducts = productsCount > products.length + 1

  const loadMoreProducts = () => {
    if (loading || loadingMoreProducts) return
    fetchMore({
      variables: {
        start: products.length,
        limit: 12
      }
    })
  }

  const loader = <div>Loading...</div>

  return (
    <section>
      <InfiniteScroll
        pageStart={0}
        loadMore={loadMoreProducts}
        hasMore={areMoreProducts}
        loader={loader}
      >
        <ul>
          {products.map((product, index) => (
            <li key={product.id} style={{ height: 100, border: '1px solid' }}>
              <div>{product.name}</div>
            </li>
          ))}
        </ul>
      </InfiniteScroll>
    </section>
  )
}

benjaminadk avatar Oct 09 '20 04:10 benjaminadk

I have very similar issue. Looks like loadMore is firing continuously till the result is received.

petrovy4 avatar Oct 13 '20 14:10 petrovy4

i have same problem, please help me

arindamsamanta748 avatar Jan 18 '21 15:01 arindamsamanta748

same problem

abhishkekalia avatar Feb 04 '21 09:02 abhishkekalia

I have the same problem, but I set hasMore=false while loading and set it back to true after load which solved this problem.

foxty avatar Feb 09 '21 08:02 foxty

@Surangaup 's solution worked for me :slightly_smiling_face:

wnikola avatar May 28 '21 14:05 wnikola