import { router } from '@inertiajs/react'; import { useCallback, useEffect, useRef, useState } from 'react'; type PaginatedData = { data: T[]; current_page: number; last_page: number; per_page: number; total: number; }; type UseInfiniteScrollOptions = { initialData: PaginatedData; fetchUrl: string; perPage?: number; }; export function useInfiniteScroll({ initialData, fetchUrl, perPage = 20, }: UseInfiniteScrollOptions) { const [items, setItems] = useState(initialData.data); const [currentPage, setCurrentPage] = useState(initialData.current_page); const [lastPage, setLastPage] = useState(initialData.last_page); const [loading, setLoading] = useState(false); const sentinelRef = useRef(null); const loadMore = useCallback(() => { if (loading || currentPage >= lastPage) { return; } setLoading(true); const url = new URL(fetchUrl, window.location.origin); url.searchParams.set('page', String(currentPage + 1)); url.searchParams.set('per_page', String(perPage)); router.get( url.pathname + url.search, {}, { preserveState: true, replace: true, only: ['mutations'], onSuccess: (page: any) => { const newMutations = (page.props as Record) .mutations as PaginatedData; setItems((prev) => [...prev, ...newMutations.data]); setCurrentPage(newMutations.current_page); setLastPage(newMutations.last_page); setLoading(false); }, onError: () => { setLoading(false); }, }, ); }, [fetchUrl, currentPage, lastPage, loading, perPage]); useEffect(() => { const sentinel = sentinelRef.current; if (!sentinel) { return; } const observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting) { loadMore(); } }, { threshold: 0.1 }, ); observer.observe(sentinel); return () => observer.disconnect(); }, [loadMore]); const hasNextPage = currentPage < lastPage; return { items, loading, hasNextPage, sentinelRef, }; }