synor/apps/explorer-web/src/hooks/useInfiniteBlocks.ts
Gulshan Yadav 48949ebb3f Initial commit: Synor blockchain monorepo
A complete blockchain implementation featuring:
- synord: Full node with GHOSTDAG consensus
- explorer-web: Modern React blockchain explorer with 3D DAG visualization
- CLI wallet and tools
- Smart contract SDK and example contracts (DEX, NFT, token)
- WASM crypto library for browser/mobile
2026-01-08 05:22:17 +05:30

115 lines
2.9 KiB
TypeScript

/**
* 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<void>;
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<ExplorerBlock[]>([]);
const [isLoading, setIsLoading] = useState(initialLoad);
const [isLoadingMore, setIsLoadingMore] = useState(false);
const [error, setError] = useState<Error | null>(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;