petyosi / react-virtuoso

The most powerful virtual list component for React
https://virtuoso.dev
MIT License
5.25k stars 301 forks source link

[BUG] startReached doesn't work with VirtuosoGrid if initialItemCount has passed #910

Closed studentIvan closed 1 year ago

studentIvan commented 1 year ago

Describe the bug

In the example code below everything works well

<VirtuosoGrid
        ref={ref}
        data={items}
        overscan={0}
        className={style.categoryGrid}
        rangeChanged={rangeCallback}
        startReached={startReached}
        endReached={endReached}
        useWindowScroll
        totalCount={category.totalHits}
       //...
/>

But if I add

initialItemCount={category.hitsPerPage}

startReached never fire, works well for endReached though. Thanks :)

Desktop (please complete the following information):

petyosi commented 1 year ago

Can you test with this branch?

studentIvan commented 1 year ago

@petyosi thanks! Is good that you supporting the grid. I see the initialTopMostItemIndex there also. Without this feature its a bit complex to work with the endless reverse scrolling. Will test it soon.

stateChanged and restoreStateFrom also sounds crazy. Would be good to have it.


For people who stuck with such problem I can share my way to solve it with atTopStateChange:

/**
   * firstItemIndex allows to detect the numeric index of
   * the first item in the rangeCallback (initialPosition)
   *
   * e.g. page=2, 28 items per page:
   * firstItemIndex = (2 - 1) * 28 = 28
   * intialPosition of the first range callback = 0 + 28 + 1 = 29
   * so the item we see first is 29th item of this category items list
   */
 const [firstItemIndex, setFirstItemIndex] = useState(
    currentPage > 1 ? (currentPage - 1) * category.hitsPerPage : 0
  );
/**
   * we choose VirtuosoGrid because it allows us to use it both with desktop and mobile
   *
   * normal startReach + firstItemIndex reverse endless scrolling combination
   * does not work with the VirtuosoGrid (since it has no firstItemIndex)
   * @see https://virtuoso.dev/virtuoso-grid-api-reference/
   *
   * we using the hack here, what locks the page calculation and uses
   * atTopStateChange event to detect atTop state (true by default)
   *
   * `scrollTopLock` is true by default when the page > 1
   * we calc it by `firstItemIndex > 0` instead of `currentPage > 1` here
   * since currentPage is a store value (dynamical) not a simple variable
   * `firstItemIndex` is coming from the local state (like normal variable)
   *
   * so if the page > 1 and atTop is true (default scenario for any page > 1 first render)
   * it calls the function "startReached" defined below
   *
   * scrollTopLock blocks the rangeCallback to reduce the risk of the currentPage changing
   * "startReached" unlocks it normally right after the algolia data fetched
   *
   * since we use a hack - the page can have some layout shifts when calling with ?page=
   * this is a payment of this hack
   *
   * normal user should not use ?page= directly, it mainly using by the crawlers
   */
const [scrollTopLock, setScrollTopLock] = useState(firstItemIndex > 0);

//...

const startReached = useCallback(() => {
    const nextFirstItemIndex = firstItemIndex - category.hitsPerPage;
    const prevPage = currentPage - 1;
    if (prevPage >= 1) {
      const setPrevPageProducts = (products: CategoryProduct[]) => {
        dispatch({
          type: ACTION.ADD_PAGE_PRODUCTS,
          payload: { pageNumber: prevPage, products },
        });
      };
      setTimeout(() => {
        setPrevPageProducts(new Array(category.hitsPerPage));
        setFirstItemIndex(() => (nextFirstItemIndex <= 0 ? 0 : nextFirstItemIndex));
        // now we have the initialTopMostItemIndex
        setTimeout(() => ref.current?.scrollToIndex(category.hitsPerPage + 2));
        setTimeout(
          () =>
            requestProductsPage(prevPage)
              .then(setPrevPageProducts)
              .then(() => setScrollTopLock(false)),
          80
        );
      }, 100);
    }
  }, [items, currentPage, category.totalPages]);

//..
<VirtuosoGrid
    //...
    atTopStateChange={
          firstItemIndex > 0
            ? (atTop) => {
                /** first init */
                if (scrollTopLock && atTop) {
                  startReached();
                }
                if (!scrollTopLock && atTop && currentPage >= 2) {
                  /** free start reach */
                  setScrollTopLock(true);
                  startReached();
                }
              }
            : undefined
        }
/>
sydneyjodon-wk commented 1 year ago

I ran into a similar issue with endReached not working if initialItemCount is set on Virtuoso (sandbox example)

petyosi commented 1 year ago

@sydneyjodon-wk what you have is not a valid configuration and not related to this thread. There are several threads that discuss a setup similar to yours.

studentIvan commented 1 year ago

@petyosi the stable branch (what is now the grid enhancement) still has this bug

petyosi commented 1 year ago

@studentIvan this discussion has gone through several iterations. Can you please help me by reproducing the current problem you experience in a sandbox? Thanks.