diff --git a/resources/js/features/game-list/components/GameListItems/GameListItems.test.tsx b/resources/js/features/game-list/components/GameListItems/GameListItems.test.tsx index 9a04dfc90f..d127ad1073 100644 --- a/resources/js/features/game-list/components/GameListItems/GameListItems.test.tsx +++ b/resources/js/features/game-list/components/GameListItems/GameListItems.test.tsx @@ -294,4 +294,107 @@ describe('Component: GameListItems', () => { expect(screen.getByText(/game 4/i)).toBeVisible(); }); + + it( + 'given the user scrolls to the bottom multiple times, only prefetches each page once', + { timeout: 10_000 }, + async () => { + // ARRANGE + mockAllIsIntersecting(false); + + const firstPageGames = [ + createGameListEntry({ game: createGame({ title: 'Game 1' }) }), + createGameListEntry({ game: createGame({ title: 'Game 2' }) }), + ]; + const secondPageGames = [ + createGameListEntry({ game: createGame({ title: 'Game 3' }) }), + createGameListEntry({ game: createGame({ title: 'Game 4' }) }), + ]; + const thirdPageGames = [ + createGameListEntry({ game: createGame({ title: 'Game 5' }) }), + createGameListEntry({ game: createGame({ title: 'Game 6' }) }), + ]; + + const firstPageData: App.Data.PaginatedData = { + currentPage: 1, + lastPage: 3, + perPage: 2, + total: 6, + unfilteredTotal: 6, + items: firstPageGames, + links: { firstPageUrl: '#', lastPageUrl: '#', nextPageUrl: '#', previousPageUrl: '#' }, + }; + + const secondPageData: App.Data.PaginatedData = { + currentPage: 2, + lastPage: 3, + perPage: 2, + total: 6, + unfilteredTotal: 6, + items: secondPageGames, + links: { firstPageUrl: '#', lastPageUrl: '#', nextPageUrl: '#', previousPageUrl: '#' }, + }; + + const thirdPageData: App.Data.PaginatedData = { + currentPage: 3, + lastPage: 3, + perPage: 2, + total: 6, + unfilteredTotal: 6, + items: thirdPageGames, + links: { firstPageUrl: '#', lastPageUrl: '#', nextPageUrl: '#', previousPageUrl: '#' }, + }; + + const getSpy = vi + .spyOn(axios, 'get') + .mockResolvedValueOnce({ data: firstPageData }) + .mockResolvedValueOnce({ data: secondPageData }) + .mockResolvedValueOnce({ data: thirdPageData }); + + render( + + + , + { + pageProps: { + ziggy: createZiggyProps({ device: 'mobile' }), + }, + }, + ); + + // ... wait for the initial load ... + await waitFor(() => { + screen.getByText(/game 1/i); + }); + + // ACT + // ... simulate scrolling to the bottom the first time ... + mockAllIsIntersecting(true); + + // ... wait for the prefetch to complete ... + await waitFor(() => { + expect(getSpy).toHaveBeenCalledTimes(2); + }); + + // ... simulate scrolling back up ... + mockAllIsIntersecting(false); + + // ... simulate scrolling to the bottom again ... + mockAllIsIntersecting(true); + + // ASSERT + /** + * The spy should still only have been called twice - once for the initial load + * and once for the prefetch. Scrolling to the bottom again shouldn't trigger + * another prefetch of the same page or the Nth+1 page. + */ + await waitFor(() => { + expect(getSpy).toHaveBeenCalledTimes(2); + }); + }, + ); }); diff --git a/resources/js/features/game-list/components/GameListItems/GameListItems.tsx b/resources/js/features/game-list/components/GameListItems/GameListItems.tsx index eaa177ea0e..8f0f50d92d 100644 --- a/resources/js/features/game-list/components/GameListItems/GameListItems.tsx +++ b/resources/js/features/game-list/components/GameListItems/GameListItems.tsx @@ -59,7 +59,7 @@ const GameListItems: FC = ({ }); const [visiblePageNumbers, setVisiblePageNumbers] = useState([1]); - + const [prefetchedPageNumbers, setPrefetchedPageNumbers] = useState([1]); const [isLoadingMore, setIsLoadingMore] = useState(false); const isEmpty = dataInfiniteQuery.data?.pages?.[0].total === 0; @@ -74,9 +74,17 @@ const GameListItems: FC = ({ const isLastPageResultsVisible = visiblePageNumbers[visiblePageNumbers.length - 1] === lastPageNumber; - const handleLoadMore = () => { + const handleLoadMore = async () => { if (dataInfiniteQuery.hasNextPage && !dataInfiniteQuery.isFetchingNextPage) { - dataInfiniteQuery.fetchNextPage(); + const nextPageNumber = Math.max(...visiblePageNumbers) + 1; + + // Don't prefetch the next page if it has already been prefetched. + if (prefetchedPageNumbers.includes(nextPageNumber)) { + return; + } + + await dataInfiniteQuery.fetchNextPage(); + setPrefetchedPageNumbers([...prefetchedPageNumbers, nextPageNumber]); } };