Files
ln-bi/src/modules/assets/components/analysis/useProgressiveRows.ts
T

53 lines
2.4 KiB
TypeScript

import { useCallback, useEffect, useMemo, useRef, useState, type RefObject } from 'react';
interface ProgressiveRowsOptions {
total: number;
rootRef: RefObject<HTMLElement | null>;
resetKey: unknown;
enabled?: boolean;
batchSize?: number;
preserveOnDisable?: boolean;
}
/** Render a cumulative prefix, loading another batch near the actual scroll body's bottom. */
export function useProgressiveRows({ total, rootRef, resetKey, enabled = true, batchSize = 40, preserveOnDisable = false }: ProgressiveRowsOptions) {
const resetEnabled = preserveOnDisable || enabled;
const context = useMemo(() => ({ resetKey, enabled: resetEnabled, batchSize }), [resetKey, resetEnabled, batchSize]);
const scrollContext = useRef(context);
const [progress, setProgress] = useState(() => ({ context, limit: batchSize }));
const [sentinel, setSentinel] = useState<HTMLDivElement | null>(null);
const visibleCount = Math.min(total, progress.context === context ? progress.limit : batchSize);
const hasMore = visibleCount < total;
const loadMore = useCallback(() => {
if (!enabled) return;
setProgress(previous => ({ context, limit: Math.min(total, (previous.context === context ? previous.limit : batchSize) + batchSize) }));
}, [enabled, context, total, batchSize]);
useEffect(() => {
if (enabled && rootRef.current && (!preserveOnDisable || scrollContext.current !== context)) rootRef.current.scrollTop = 0;
scrollContext.current = context;
}, [context, enabled, rootRef, preserveOnDisable]);
useEffect(() => {
const root = rootRef.current;
if (!enabled || !hasMore || !root || !sentinel || typeof IntersectionObserver === 'undefined') return;
let active = true;
let queued = false;
let frame: number | undefined;
const observer = new IntersectionObserver(entries => {
if (!active || queued || !entries.some(entry => entry.isIntersecting)) return;
queued = true;
// One append per observer: let layout settle before deciding whether more rows fit.
frame = requestAnimationFrame(loadMore);
}, { root, rootMargin: '0px 0px 240px 0px', threshold: 0 });
observer.observe(sentinel);
return () => {
active = false;
observer.disconnect();
if (frame !== undefined) cancelAnimationFrame(frame);
};
}, [enabled, hasMore, rootRef, sentinel, visibleCount, loadMore]);
return { visibleCount, total, hasMore, loadMore, sentinelRef: setSentinel };
}