Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | 30x 30x 30x 30x 30x 30x 30x 30x 30x 30x 30x 15x 15x 15x 15x 15x 15x 15x 15x 14x 14x 14x 14x 14x 14x 14x 1x 15x 15x 15x 30x 14x 30x 27x 2x 2x 2x 2x 2x 30x 1x 30x 1x 2x 30x 3x 1x 30x | import { useState, useEffect, useRef, useCallback } from "react";
export interface InfiniteScrollOptions {
/** Number of items to load per page */
pageSize?: number;
/** Intersection observer threshold (0-1) */
threshold?: number;
}
export interface InfiniteScrollResult<T> {
/** Current items */
items: T[];
/** Whether initial load is in progress */
isLoading: boolean;
/** Whether loading more items */
isLoadingMore: boolean;
/** Whether there are more items to load */
hasMore: boolean;
/** Total count of items */
total: number;
/** Ref to attach to the trigger element */
loadMoreRef: React.RefObject<HTMLDivElement | null>;
/** Reset and reload from the beginning */
reset: () => void;
/** Update a single item in the list */
updateItem: (id: string, updater: (item: T) => T) => void;
/** Remove an item from the list */
removeItem: (id: string) => void;
}
export interface LoadFnParams {
offset: number;
limit: number;
}
export interface LoadFnResult<T> {
data: T[];
total: number;
}
/**
* Hook for infinite scroll pagination with IntersectionObserver
*
* @param loadFn - Async function to load items
* @param deps - Dependencies that should trigger a reset when changed
* @param options - Configuration options
*/
export function useInfiniteScroll<T extends { id: string }>(
loadFn: (params: LoadFnParams) => Promise<LoadFnResult<T>>,
deps: unknown[] = [],
options: InfiniteScrollOptions = {},
): InfiniteScrollResult<T> {
const { pageSize = 10, threshold = 0.1 } = options;
const [items, setItems] = useState<T[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isLoadingMore, setIsLoadingMore] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [total, setTotal] = useState(0);
const observerRef = useRef<IntersectionObserver | null>(null);
const loadMoreRef = useRef<HTMLDivElement | null>(null);
const offsetRef = useRef(0);
const isLoadingRef = useRef(false);
const load = useCallback(
async (reset = false) => {
// Prevent concurrent loads
Iif (isLoadingRef.current) return;
isLoadingRef.current = true;
if (reset) {
setIsLoading(true);
offsetRef.current = 0;
} else E{
setIsLoadingMore(true);
}
try {
const offset = reset ? 0 : offsetRef.current;
const result = await loadFn({ offset, limit: pageSize });
const newItems = result.data;
const totalCount = result.total;
if (reset) {
setItems(newItems);
} else E{
setItems((prev) => [...prev, ...newItems]);
}
offsetRef.current = offset + newItems.length;
setTotal(totalCount);
setHasMore(offset + newItems.length < totalCount);
} catch (err) {
console.error("Failed to load items:", err);
} finally {
setIsLoading(false);
setIsLoadingMore(false);
isLoadingRef.current = false;
}
},
[loadFn, pageSize],
);
// Reset when dependencies change
useEffect(() => {
load(true);
// biome-ignore lint/correctness/useExhaustiveDependencies: deps is provided externally
}, deps);
// Setup IntersectionObserver
useEffect(() => {
if (isLoading || isLoadingMore || !hasMore) return;
observerRef.current = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasMore && !isLoadingRef.current) {
load(false);
}
},
{ threshold },
);
Iif (loadMoreRef.current) {
observerRef.current.observe(loadMoreRef.current);
}
return () => {
Eif (observerRef.current) {
observerRef.current.disconnect();
}
};
}, [isLoading, isLoadingMore, hasMore, load, threshold]);
const reset = useCallback(() => {
load(true);
}, [load]);
const updateItem = useCallback((id: string, updater: (item: T) => T) => {
setItems((prev) =>
prev.map((item) => (item.id === id ? updater(item) : item)),
);
}, []);
const removeItem = useCallback((id: string) => {
setItems((prev) => prev.filter((item) => item.id !== id));
setTotal((prev) => prev - 1);
}, []);
return {
items,
isLoading,
isLoadingMore,
hasMore,
total,
loadMoreRef,
reset,
updateItem,
removeItem,
};
}
|