Add ref controls to infinite grid

This commit is contained in:
jeffvli 2022-12-24 20:19:56 -08:00
parent 520b7ce136
commit 26ea4c0cc9

View file

@ -1,4 +1,12 @@
import { useState, useEffect, useRef, useMemo, useCallback } from 'react'; import {
useState,
useRef,
useMemo,
useCallback,
forwardRef,
Ref,
useImperativeHandle,
} from 'react';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import type { FixedSizeListProps } from 'react-window'; import type { FixedSizeListProps } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader'; import InfiniteLoader from 'react-window-infinite-loader';
@ -6,6 +14,12 @@ import { VirtualGridWrapper } from '/@/renderer/components/virtual-grid/virtual-
import type { CardRoute, CardRow, LibraryItem, PlayQueueAddOptions } from '/@/renderer/types'; import type { CardRoute, CardRow, LibraryItem, PlayQueueAddOptions } from '/@/renderer/types';
import { CardDisplayType } from '/@/renderer/types'; import { CardDisplayType } from '/@/renderer/types';
export type VirtualInfiniteGridRef = {
resetLoadMoreItemsCache: () => void;
scrollTo: (index: number) => void;
setItemData: (data: any[]) => void;
};
interface VirtualGridProps extends Omit<FixedSizeListProps, 'children' | 'itemSize'> { interface VirtualGridProps extends Omit<FixedSizeListProps, 'children' | 'itemSize'> {
cardRows: CardRow<any>[]; cardRows: CardRow<any>[];
display?: CardDisplayType; display?: CardDisplayType;
@ -15,7 +29,6 @@ interface VirtualGridProps extends Omit<FixedSizeListProps, 'children' | 'itemSi
itemSize: number; itemSize: number;
itemType: LibraryItem; itemType: LibraryItem;
minimumBatchSize?: number; minimumBatchSize?: number;
refresh?: any;
route?: CardRoute; route?: CardRoute;
} }
@ -27,131 +40,138 @@ const constrainWidth = (width: number) => {
return 1920; return 1920;
}; };
export const VirtualInfiniteGrid = ({ export const VirtualInfiniteGrid = forwardRef(
itemCount, (
itemGap, {
itemSize, itemCount,
itemType, itemGap,
cardRows, itemSize,
route, itemType,
onScroll, cardRows,
display, route,
handlePlayQueueAdd, onScroll,
minimumBatchSize, display,
fetchFn, handlePlayQueueAdd,
initialScrollOffset, minimumBatchSize,
height, fetchFn,
width, initialScrollOffset,
refresh, height,
}: VirtualGridProps) => { width,
const [itemData, setItemData] = useState<any[]>([]); }: VirtualGridProps,
const listRef = useRef<any>(null); ref: Ref<VirtualInfiniteGridRef>,
const loader = useRef<InfiniteLoader>(null); ) => {
const [itemData, setItemData] = useState<any[]>([]);
const listRef = useRef<any>(null);
const loader = useRef<InfiniteLoader>(null);
const { itemHeight, rowCount, columnCount } = useMemo(() => { const { itemHeight, rowCount, columnCount } = useMemo(() => {
const itemsPerRow = Math.floor( const itemsPerRow = Math.floor(
(constrainWidth(Number(width)) - itemGap + 3) / (itemSize! + itemGap + 2), (constrainWidth(Number(width)) - itemGap + 3) / (itemSize! + itemGap + 2),
);
return {
columnCount: itemsPerRow,
itemHeight: itemSize! + cardRows.length * 22 + itemGap,
itemWidth: itemSize! + itemGap,
rowCount: Math.ceil(itemCount / itemsPerRow),
};
}, [cardRows.length, itemCount, itemGap, itemSize, width]);
const isItemLoaded = useCallback(
(index: number) => {
const itemIndex = index * columnCount;
return itemData[itemIndex] !== undefined;
},
[columnCount, itemData],
); );
return { const loadMoreItems = useCallback(
columnCount: itemsPerRow, async (startIndex: number, stopIndex: number) => {
itemHeight: itemSize! + cardRows.length * 22 + itemGap, // Fixes a caching bug(?) when switching between filters and the itemCount increases
itemWidth: itemSize! + itemGap, if (startIndex === 1) return;
rowCount: Math.ceil(itemCount / itemsPerRow),
};
}, [cardRows.length, itemCount, itemGap, itemSize, width]);
const isItemLoaded = useCallback( // Need to multiply by columnCount due to the grid layout
(index: number) => { const start = startIndex * columnCount;
const itemIndex = index * columnCount; const end = stopIndex * columnCount + columnCount;
return itemData[itemIndex] !== undefined; const data = await fetchFn({
}, columnCount,
[columnCount, itemData], skip: start,
); take: end - start,
});
const loadMoreItems = useCallback( const newData: any[] = [...itemData];
async (startIndex: number, stopIndex: number) => {
// Fixes a caching bug(?) when switching between filters and the itemCount increases
if (startIndex === 1) return;
// Need to multiply by columnCount due to the grid layout let itemIndex = 0;
const start = startIndex * columnCount; for (let rowIndex = start; rowIndex < end; rowIndex += 1) {
const end = stopIndex * columnCount + columnCount; newData[rowIndex] = data.items[itemIndex];
itemIndex += 1;
}
const data = await fetchFn({ setItemData(newData);
columnCount, },
skip: start, [columnCount, fetchFn, itemData],
take: end - start, );
});
const newData: any[] = [...itemData]; const debouncedLoadMoreItems = debounce(loadMoreItems, 500);
let itemIndex = 0; useImperativeHandle(ref, () => ({
for (let rowIndex = start; rowIndex < end; rowIndex += 1) { resetLoadMoreItemsCache: () => {
newData[rowIndex] = data.items[itemIndex]; if (loader.current) {
itemIndex += 1; loader.current.resetloadMoreItemsCache(false);
} setItemData(() => []);
}
},
scrollTo: (index: number) => {
listRef.current.scrollToItem(index);
},
setItemData: (data: any[]) => {
setItemData(data);
},
}));
setItemData(newData); return (
}, <InfiniteLoader
[columnCount, fetchFn, itemData], ref={loader}
); isItemLoaded={(index) => isItemLoaded(index)}
itemCount={itemCount || 0}
const debouncedLoadMoreItems = debounce(loadMoreItems, 500); loadMoreItems={debouncedLoadMoreItems}
minimumBatchSize={minimumBatchSize}
useEffect(() => { threshold={30}
if (loader.current) { >
listRef.current.scrollTo(0); {({ onItemsRendered, ref: infiniteLoaderRef }) => (
loader.current.resetloadMoreItemsCache(false); <VirtualGridWrapper
setItemData(() => []); cardRows={cardRows}
columnCount={columnCount}
loadMoreItems(0, minimumBatchSize! * 2); display={display || CardDisplayType.CARD}
} handlePlayQueueAdd={handlePlayQueueAdd}
// eslint-disable-next-line react-hooks/exhaustive-deps height={height}
}, [minimumBatchSize, fetchFn, refresh]); initialScrollOffset={initialScrollOffset}
itemCount={itemCount || 0}
return ( itemData={itemData}
<InfiniteLoader itemGap={itemGap}
ref={loader} itemHeight={itemHeight + itemGap / 2}
isItemLoaded={(index) => isItemLoaded(index)} itemType={itemType}
itemCount={itemCount || 0} itemWidth={itemSize}
loadMoreItems={debouncedLoadMoreItems} refInstance={(list) => {
minimumBatchSize={minimumBatchSize} infiniteLoaderRef(list);
threshold={30} listRef.current = list;
> }}
{({ onItemsRendered, ref: infiniteLoaderRef }) => ( route={route}
<VirtualGridWrapper rowCount={rowCount}
cardRows={cardRows} width={width}
columnCount={columnCount} onItemsRendered={onItemsRendered}
display={display || CardDisplayType.CARD} onScroll={onScroll}
handlePlayQueueAdd={handlePlayQueueAdd} />
height={height} )}
initialScrollOffset={initialScrollOffset} </InfiniteLoader>
itemCount={itemCount || 0} );
itemData={itemData} },
itemGap={itemGap} );
itemHeight={itemHeight + itemGap / 2}
itemType={itemType}
itemWidth={itemSize}
refInstance={(list) => {
infiniteLoaderRef(list);
listRef.current = list;
}}
route={route}
rowCount={rowCount}
width={width}
onItemsRendered={onItemsRendered}
onScroll={onScroll}
/>
)}
</InfiniteLoader>
);
};
VirtualInfiniteGrid.defaultProps = { VirtualInfiniteGrid.defaultProps = {
display: CardDisplayType.CARD, display: CardDisplayType.CARD,
minimumBatchSize: 20, minimumBatchSize: 20,
refresh: undefined,
route: undefined, route: undefined,
}; };