53 lines
2.4 KiB
TypeScript
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 };
|
|
}
|