UNPKG

masonic

Version:

<hr> <div align="center"> <h1 align="center"> 🧱 masonic </h1>

178 lines (162 loc) • 5.61 kB
import useLatest from "@react-hook/latest"; import * as React from "react"; /** * A utility hook for seamlessly adding infinite scroll behavior to the `useMasonry()` hook. This * hook invokes a callback each time the last rendered index surpasses the total number of items * in your items array or the number defined in the `totalItems` option. * * @param loadMoreItems - This callback is invoked when more rows must be loaded. It will be used to * determine when to refresh the list with the newly-loaded data. This callback may be called multiple * times in reaction to a single scroll event, so it's important to memoize its arguments. If you're * creating this callback inside of a functional component, make sure you wrap it in `React.useCallback()`, * as well. * @param options */ export function useInfiniteLoader<Item, T extends LoadMoreItemsCallback<Item>>( loadMoreItems: T, options: UseInfiniteLoaderOptions<Item> = emptyObj ): LoadMoreItemsCallback<Item> { const { isItemLoaded, minimumBatchSize = 16, threshold = 16, totalItems = 9e9, } = options; const storedLoadMoreItems = useLatest(loadMoreItems); const storedIsItemLoaded = useLatest(isItemLoaded); return React.useCallback( (startIndex, stopIndex, items) => { const unloadedRanges = scanForUnloadedRanges( storedIsItemLoaded.current, minimumBatchSize, items, totalItems, Math.max(0, startIndex - threshold), Math.min(totalItems - 1, (stopIndex || 0) + threshold) ); // The user is responsible for memoizing their loadMoreItems() function // because we don't want to make assumptions about how they want to deal // with `items` for (let i = 0; i < unloadedRanges.length - 1; ++i) storedLoadMoreItems.current( unloadedRanges[i], unloadedRanges[++i], items ); }, [ totalItems, minimumBatchSize, threshold, storedLoadMoreItems, storedIsItemLoaded, ] ); } /** * Returns all of the ranges within a larger range that contain unloaded rows. * * @param isItemLoaded * @param minimumBatchSize * @param items * @param totalItems * @param startIndex * @param stopIndex */ function scanForUnloadedRanges<Item>( isItemLoaded: UseInfiniteLoaderOptions<Item>["isItemLoaded"] = defaultIsItemLoaded, minimumBatchSize: UseInfiniteLoaderOptions<Item>["minimumBatchSize"] = 16, items: any[], totalItems: UseInfiniteLoaderOptions<Item>["totalItems"] = 9e9, startIndex: number, stopIndex: number ): number[] { const unloadedRanges: number[] = []; let rangeStartIndex: number | undefined, rangeStopIndex: number | undefined, index = startIndex; /* istanbul ignore next */ for (; index <= stopIndex; index++) { if (!isItemLoaded(index, items)) { rangeStopIndex = index; if (rangeStartIndex === void 0) rangeStartIndex = index; } else if (rangeStartIndex !== void 0 && rangeStopIndex !== void 0) { unloadedRanges.push(rangeStartIndex, rangeStopIndex); rangeStartIndex = rangeStopIndex = void 0; } } // If :rangeStopIndex is not null it means we haven't run out of unloaded rows. // Scan forward to try filling our :minimumBatchSize. if (rangeStartIndex !== void 0 && rangeStopIndex !== void 0) { const potentialStopIndex = Math.min( Math.max(rangeStopIndex, rangeStartIndex + minimumBatchSize - 1), totalItems - 1 ); /* istanbul ignore next */ for (index = rangeStopIndex + 1; index <= potentialStopIndex; index++) { if (!isItemLoaded(index, items)) { rangeStopIndex = index; } else { break; } } unloadedRanges.push(rangeStartIndex, rangeStopIndex); } // Check to see if our first range ended prematurely. // In this case we should scan backwards to try filling our :minimumBatchSize. /* istanbul ignore next */ if (unloadedRanges.length) { let firstUnloadedStart = unloadedRanges[0]; const firstUnloadedStop = unloadedRanges[1]; while ( firstUnloadedStop - firstUnloadedStart + 1 < minimumBatchSize && firstUnloadedStart > 0 ) { const index = firstUnloadedStart - 1; if (!isItemLoaded(index, items)) { unloadedRanges[0] = firstUnloadedStart = index; } else { break; } } } return unloadedRanges; } const defaultIsItemLoaded = <Item>(index: number, items: Item[]): boolean => items[index] !== void 0; export interface UseInfiniteLoaderOptions<Item> { /** * A callback responsible for determining the loaded state of each item. Should return `true` * if the item has already been loaded and `false` if not. * * @default (index: number, items: any[]) => boolean */ isItemLoaded?: (index: number, items: Item[]) => boolean; /** * The minimum number of new items to be loaded at a time. This property can be used to * batch requests and reduce HTTP requests. * * @default 16 */ minimumBatchSize?: number; /** * The threshold at which to pre-fetch data. A threshold X means that new data should start * loading when a user scrolls within X cells of the end of your `items` array. * * @default 16 */ threshold?: number; /** * The total number of items you'll need to eventually load (if known). This can * be arbitrarily high if not known. * * @default 9e9 */ totalItems?: number; } export type LoadMoreItemsCallback<Item> = ( startIndex: number, stopIndex: number, items: Item[] ) => any; const emptyObj = {};