/** * Infinite loading hook for blocks. * Accumulates blocks across multiple pages for virtual scrolling. */ import { useState, useCallback, useRef, useEffect } from 'react'; import { api } from '../lib/api'; import type { ExplorerBlock } from '../lib/types'; interface UseInfiniteBlocksResult { blocks: ExplorerBlock[]; isLoading: boolean; isLoadingMore: boolean; error: Error | null; hasMore: boolean; loadMore: () => Promise; reset: () => void; total: number; } interface UseInfiniteBlocksOptions { pageSize?: number; initialLoad?: boolean; } export function useInfiniteBlocks( options: UseInfiniteBlocksOptions = {} ): UseInfiniteBlocksResult { const { pageSize = 50, initialLoad = true } = options; const [blocks, setBlocks] = useState([]); const [isLoading, setIsLoading] = useState(initialLoad); const [isLoadingMore, setIsLoadingMore] = useState(false); const [error, setError] = useState(null); const [hasMore, setHasMore] = useState(true); const [total, setTotal] = useState(0); // Track current page to avoid duplicate fetches const currentPageRef = useRef(0); const isFetchingRef = useRef(false); const fetchPage = useCallback( async (page: number, isInitial = false) => { if (isFetchingRef.current) return; isFetchingRef.current = true; try { if (isInitial) { setIsLoading(true); } else { setIsLoadingMore(true); } setError(null); const response = await api.getBlocks(page, pageSize); setBlocks((prev) => { // For initial load, replace blocks if (isInitial) return response.data; // For subsequent loads, append (avoiding duplicates) const existingHashes = new Set(prev.map((b) => b.hash)); const newBlocks = response.data.filter( (b) => !existingHashes.has(b.hash) ); return [...prev, ...newBlocks]; }); setTotal(response.total); setHasMore(response.hasNext); currentPageRef.current = page; } catch (e) { setError(e instanceof Error ? e : new Error(String(e))); } finally { setIsLoading(false); setIsLoadingMore(false); isFetchingRef.current = false; } }, [pageSize] ); // Initial load useEffect(() => { if (initialLoad) { fetchPage(1, true); } }, [fetchPage, initialLoad]); const loadMore = useCallback(async () => { if (isFetchingRef.current || !hasMore) return; const nextPage = currentPageRef.current + 1; await fetchPage(nextPage); }, [fetchPage, hasMore]); const reset = useCallback(() => { setBlocks([]); setHasMore(true); setTotal(0); currentPageRef.current = 0; fetchPage(1, true); }, [fetchPage]); return { blocks, isLoading, isLoadingMore, error, hasMore, loadMore, reset, total, }; } export default useInfiniteBlocks;