import { QueryKey, useInfiniteQuery } from "@tanstack/react-query" import { ReactNode, useEffect, useMemo, useRef } from "react" import { toast } from "@medusajs/ui" import { Spinner } from "@medusajs/icons" type InfiniteListProps = { queryKey: QueryKey queryFn: (params: TParams) => Promise queryOptions?: { enabled?: boolean } renderItem: (item: TEntity) => ReactNode renderEmpty: () => ReactNode responseKey: keyof TResponse pageSize?: number } export const InfiniteList = < TResponse extends { count: number; offset: number; limit: number }, TEntity extends { id: string }, TParams extends { offset?: number; limit?: number } >({ queryKey, queryFn, queryOptions, renderItem, renderEmpty, responseKey, pageSize = 20, }: InfiniteListProps) => { const { data, error, fetchNextPage, fetchPreviousPage, hasPreviousPage, hasNextPage, isFetching, isPending, } = useInfiniteQuery({ queryKey: queryKey, queryFn: async ({ pageParam = 0 }) => { return await queryFn({ limit: pageSize, offset: pageParam, } as TParams) }, initialPageParam: 0, maxPages: 5, getNextPageParam: (lastPage) => { const moreItemsExist = lastPage.count > lastPage.offset + lastPage.limit return moreItemsExist ? lastPage.offset + lastPage.limit : undefined }, getPreviousPageParam: (firstPage) => { const moreItemsExist = firstPage.offset !== 0 return moreItemsExist ? Math.max(firstPage.offset - firstPage.limit, 0) : undefined }, ...queryOptions, }) const items = useMemo(() => { return data?.pages.flatMap((p) => p[responseKey] as TEntity[]) ?? [] }, [data, responseKey]) const parentRef = useRef(null) const startObserver = useRef() const endObserver = useRef() const fetchNextPageRef = useRef(fetchNextPage) const fetchPreviousPageRef = useRef(fetchPreviousPage) useEffect(() => { fetchNextPageRef.current = fetchNextPage fetchPreviousPageRef.current = fetchPreviousPage }, [fetchNextPage, fetchPreviousPage]) useEffect(() => { if (isPending) { return } // Define the new observers after we stop fetching if (!isFetching) { // Define the new observers after paginating startObserver.current = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting && hasPreviousPage) { startObserver.current?.disconnect() fetchPreviousPageRef.current() } }, { threshold: 0.5, } ) endObserver.current = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting && hasNextPage) { endObserver.current?.disconnect() fetchNextPageRef.current() } }, { threshold: 0.5, } ) // Register the new observers to observe the new first and last children if (parentRef.current?.firstChild) { startObserver.current?.observe(parentRef.current.firstChild as Element) } if (parentRef.current?.lastChild) { endObserver.current?.observe(parentRef.current.lastChild as Element) } } // Clear the old observers return () => { startObserver.current?.disconnect() endObserver.current?.disconnect() } }, [hasNextPage, hasPreviousPage, isFetching, isPending]) useEffect(() => { if (error) { toast.error(error.message) } }, [error]) if (isPending) { return (
) } return (
{items?.length ? items.map((item) =>
{renderItem(item)}
) : renderEmpty()} {isFetching && (
)}
) }