mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 12:30:12 +02:00
use throttle for item loader, simplify implementation
This commit is contained in:
@@ -4,7 +4,8 @@ import {
|
||||
useSuspenseQuery,
|
||||
UseSuspenseQueryOptions,
|
||||
} from '@tanstack/react-query';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import throttle from 'lodash/throttle';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { useListContext } from '/@/renderer/context/list-context';
|
||||
@@ -38,7 +39,6 @@ interface UseItemListInfiniteLoaderProps {
|
||||
itemType: LibraryItem;
|
||||
listCountQuery: UseSuspenseQueryOptions<number, Error, number, readonly unknown[]>;
|
||||
listQueryFn: (args: { apiClientProps: any; query: any }) => Promise<{ items: unknown[] }>;
|
||||
maxPagesToFetch?: number;
|
||||
query: Record<string, any>;
|
||||
serverId: string;
|
||||
}
|
||||
@@ -57,17 +57,11 @@ export const useItemListInfiniteLoader = ({
|
||||
itemType,
|
||||
listCountQuery,
|
||||
listQueryFn,
|
||||
maxPagesToFetch = 2,
|
||||
query = {},
|
||||
serverId,
|
||||
}: UseItemListInfiniteLoaderProps) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const scrollStateRef = useRef<ScrollState>({
|
||||
direction: 'unknown',
|
||||
lastStartIndex: null,
|
||||
});
|
||||
|
||||
const { data: totalItemCount } = useSuspenseQuery<number, any, number, any>(listCountQuery);
|
||||
|
||||
const { setItemCount } = useListContext();
|
||||
@@ -105,75 +99,44 @@ export const useItemListInfiniteLoader = ({
|
||||
queryKey: dataQueryKey,
|
||||
});
|
||||
|
||||
const onRangeChanged = useMemo(() => {
|
||||
return async (range: { startIndex: number; stopIndex: number }) => {
|
||||
const fetchRange = getFetchRange(
|
||||
range,
|
||||
scrollStateRef,
|
||||
itemsPerPage,
|
||||
maxPagesToFetch,
|
||||
fetchThreshold,
|
||||
);
|
||||
const fetchPage = useCallback(
|
||||
async (pageNumber: number) => {
|
||||
const startIndex = pageNumber * itemsPerPage;
|
||||
const queryParams = {
|
||||
limit: itemsPerPage,
|
||||
startIndex,
|
||||
...query,
|
||||
};
|
||||
|
||||
// Filter out pages that are already loaded
|
||||
const pagesToFetch = fetchRange.pagesToFetch.filter(
|
||||
(pageNumber) => !data.pagesLoaded[pageNumber],
|
||||
);
|
||||
const result = await queryClient.ensureQueryData({
|
||||
gcTime: 1000 * 15,
|
||||
queryFn: async ({ signal }) => {
|
||||
const result = await listQueryFn({
|
||||
apiClientProps: { serverId, signal },
|
||||
query: queryParams,
|
||||
});
|
||||
|
||||
if (pagesToFetch.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create fetch promises for all pages
|
||||
const fetchPromises = pagesToFetch.map(async (pageNumber) => {
|
||||
const startIndex = pageNumber * itemsPerPage;
|
||||
const queryParams = {
|
||||
limit: itemsPerPage,
|
||||
startIndex,
|
||||
...query,
|
||||
};
|
||||
|
||||
const result = await queryClient.ensureQueryData({
|
||||
gcTime: 1000 * 15,
|
||||
queryFn: async ({ signal }) => {
|
||||
const result = await listQueryFn({
|
||||
apiClientProps: { serverId, signal },
|
||||
query: queryParams,
|
||||
});
|
||||
|
||||
return result.items;
|
||||
},
|
||||
queryKey: queryKeys[getQueryKeyName(itemType)].list(serverId, queryParams),
|
||||
staleTime: 1000 * 15,
|
||||
});
|
||||
|
||||
return {
|
||||
data: result,
|
||||
endIndex: startIndex + itemsPerPage,
|
||||
pageNumber,
|
||||
startIndex,
|
||||
};
|
||||
return result.items;
|
||||
},
|
||||
queryKey: queryKeys[getQueryKeyName(itemType)].list(serverId, queryParams),
|
||||
staleTime: 1000 * 15,
|
||||
});
|
||||
|
||||
// Wait for all pages to be fetched
|
||||
const pageResults = await Promise.all(fetchPromises);
|
||||
const endIndex = startIndex + itemsPerPage;
|
||||
|
||||
// Update the query data with all fetched pages
|
||||
// Update the query data with the fetched page
|
||||
queryClient.setQueryData(
|
||||
dataQueryKey,
|
||||
(oldData: { data: unknown[]; pagesLoaded: Record<string, boolean> }) => {
|
||||
let newData = [...oldData.data];
|
||||
const newPagesLoaded = { ...oldData.pagesLoaded };
|
||||
|
||||
// Update data for each fetched page
|
||||
pageResults.forEach(({ data: pageData, endIndex, pageNumber, startIndex }) => {
|
||||
newData = [
|
||||
...newData.slice(0, startIndex),
|
||||
...pageData,
|
||||
...newData.slice(endIndex),
|
||||
];
|
||||
newPagesLoaded[pageNumber] = true;
|
||||
});
|
||||
const newData = [
|
||||
...oldData.data.slice(0, startIndex),
|
||||
...result,
|
||||
...oldData.data.slice(endIndex),
|
||||
];
|
||||
const newPagesLoaded = {
|
||||
...oldData.pagesLoaded,
|
||||
[pageNumber]: true,
|
||||
};
|
||||
|
||||
return {
|
||||
data: newData,
|
||||
@@ -181,19 +144,63 @@ export const useItemListInfiniteLoader = ({
|
||||
};
|
||||
},
|
||||
);
|
||||
};
|
||||
}, [
|
||||
itemsPerPage,
|
||||
query,
|
||||
queryClient,
|
||||
serverId,
|
||||
dataQueryKey,
|
||||
listQueryFn,
|
||||
itemType,
|
||||
data,
|
||||
maxPagesToFetch,
|
||||
fetchThreshold,
|
||||
]);
|
||||
},
|
||||
[itemsPerPage, query, queryClient, serverId, dataQueryKey, listQueryFn, itemType],
|
||||
);
|
||||
|
||||
const onRangeChangedBase = useCallback(
|
||||
async (range: { startIndex: number; stopIndex: number }) => {
|
||||
const pageNumber = Math.floor(range.startIndex / itemsPerPage);
|
||||
|
||||
const currentData = queryClient.getQueryData<{
|
||||
data: unknown[];
|
||||
pagesLoaded: Record<string, boolean>;
|
||||
}>(dataQueryKey);
|
||||
|
||||
const startPageBoundary = pageNumber * itemsPerPage;
|
||||
const endPageBoundary = (pageNumber + 1) * itemsPerPage;
|
||||
|
||||
const distanceFromStartBoundary = range.startIndex - startPageBoundary;
|
||||
const distanceToEndBoundary = endPageBoundary - range.stopIndex;
|
||||
|
||||
const thresholdDistance = Math.floor(itemsPerPage * fetchThreshold);
|
||||
|
||||
const isCurrentPageLoaded = currentData?.pagesLoaded[pageNumber] ?? false;
|
||||
|
||||
// Fetch current page if not loaded
|
||||
if (!isCurrentPageLoaded) {
|
||||
await fetchPage(pageNumber);
|
||||
}
|
||||
|
||||
// If current page is loaded, check if we should prefetch adjacent pages
|
||||
if (isCurrentPageLoaded) {
|
||||
if (
|
||||
distanceFromStartBoundary <= thresholdDistance &&
|
||||
pageNumber > 0 &&
|
||||
!currentData?.pagesLoaded[pageNumber - 1]
|
||||
) {
|
||||
await fetchPage(pageNumber - 1);
|
||||
}
|
||||
|
||||
if (
|
||||
distanceToEndBoundary <= thresholdDistance &&
|
||||
!currentData?.pagesLoaded[pageNumber + 1]
|
||||
) {
|
||||
await fetchPage(pageNumber + 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
[itemsPerPage, fetchThreshold, queryClient, dataQueryKey, fetchPage],
|
||||
);
|
||||
|
||||
const onRangeChanged = useMemo(
|
||||
() =>
|
||||
throttle(onRangeChangedBase, 150, {
|
||||
leading: true,
|
||||
trailing: true,
|
||||
}),
|
||||
[onRangeChangedBase],
|
||||
);
|
||||
|
||||
const refresh = useCallback(
|
||||
async (force?: boolean) => {
|
||||
@@ -309,126 +316,3 @@ export const parseListCountQuery = (query: any) => {
|
||||
startIndex: 0,
|
||||
};
|
||||
};
|
||||
|
||||
interface ScrollState {
|
||||
direction: 'down' | 'unknown' | 'up';
|
||||
lastStartIndex: null | number;
|
||||
}
|
||||
|
||||
const getFetchRange = (
|
||||
range: { startIndex: number; stopIndex: number },
|
||||
scrollState: React.MutableRefObject<ScrollState>,
|
||||
itemsPerPage: number,
|
||||
maxPagesToFetch: number,
|
||||
fetchThreshold: number,
|
||||
) => {
|
||||
const { lastStartIndex } = scrollState.current;
|
||||
|
||||
// Determine scroll direction
|
||||
let newDirection: 'down' | 'unknown' | 'up' = scrollState.current.direction;
|
||||
if (lastStartIndex !== null) {
|
||||
if (range.startIndex < lastStartIndex) {
|
||||
newDirection = 'up';
|
||||
} else if (range.startIndex > lastStartIndex) {
|
||||
newDirection = 'down';
|
||||
}
|
||||
}
|
||||
|
||||
scrollState.current = {
|
||||
direction: newDirection,
|
||||
lastStartIndex: range.startIndex,
|
||||
};
|
||||
|
||||
// Calculate threshold distance
|
||||
const thresholdDistance = Math.floor(itemsPerPage * fetchThreshold);
|
||||
|
||||
// Determine which pages to fetch based on scroll direction and threshold
|
||||
let pagesToFetch: number[] = [];
|
||||
|
||||
// Calculate page boundaries for the range
|
||||
const startPage = Math.floor(range.startIndex / itemsPerPage);
|
||||
const stopPage = Math.floor(range.stopIndex / itemsPerPage);
|
||||
|
||||
// Distance from startIndex to the start of its page
|
||||
const distanceFromStartPageTop = range.startIndex - startPage * itemsPerPage;
|
||||
|
||||
// Distance from stopIndex to the end of its page (next page boundary)
|
||||
const distanceFromStopPageBottom = (stopPage + 1) * itemsPerPage - range.stopIndex;
|
||||
|
||||
if (newDirection === 'down') {
|
||||
// Always include pages in the visible range
|
||||
for (let page = startPage; page <= stopPage; page++) {
|
||||
pagesToFetch.push(page);
|
||||
}
|
||||
|
||||
// If we're close to the next page boundary below, fetch additional upcoming pages
|
||||
if (distanceFromStopPageBottom <= thresholdDistance && maxPagesToFetch > 1) {
|
||||
for (let i = 1; i < maxPagesToFetch; i++) {
|
||||
pagesToFetch.push(stopPage + i);
|
||||
}
|
||||
}
|
||||
|
||||
// If we're close to a page boundary above, fetch additional previous pages
|
||||
if (distanceFromStartPageTop <= thresholdDistance && maxPagesToFetch > 1) {
|
||||
for (let i = 1; i < maxPagesToFetch; i++) {
|
||||
const prevPage = startPage - i;
|
||||
if (prevPage >= 0) {
|
||||
pagesToFetch.push(prevPage);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (newDirection === 'up') {
|
||||
// Always include pages in the visible range
|
||||
for (let page = startPage; page <= stopPage; page++) {
|
||||
pagesToFetch.push(page);
|
||||
}
|
||||
|
||||
// If we're close to the previous page boundary above, fetch additional previous pages
|
||||
if (distanceFromStartPageTop <= thresholdDistance && maxPagesToFetch > 1) {
|
||||
for (let i = 1; i < maxPagesToFetch; i++) {
|
||||
const prevPage = startPage - i;
|
||||
if (prevPage >= 0) {
|
||||
pagesToFetch.push(prevPage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we're close to a page boundary below, fetch additional upcoming pages
|
||||
if (distanceFromStopPageBottom <= thresholdDistance && maxPagesToFetch > 1) {
|
||||
for (let i = 1; i < maxPagesToFetch; i++) {
|
||||
pagesToFetch.push(stopPage + i);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Unknown direction - fetch pages in the visible range and nearby pages
|
||||
for (let page = startPage; page <= stopPage; page++) {
|
||||
pagesToFetch.push(page);
|
||||
}
|
||||
|
||||
// Fetch additional pages above if close to boundary
|
||||
if (distanceFromStartPageTop <= thresholdDistance && maxPagesToFetch > 1) {
|
||||
for (let i = 1; i < maxPagesToFetch; i++) {
|
||||
const prevPage = startPage - i;
|
||||
if (prevPage >= 0) {
|
||||
pagesToFetch.push(prevPage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch additional pages below if close to boundary
|
||||
if (distanceFromStopPageBottom <= thresholdDistance && maxPagesToFetch > 1) {
|
||||
for (let i = 1; i < maxPagesToFetch; i++) {
|
||||
pagesToFetch.push(stopPage + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove duplicates and filter out negative page numbers
|
||||
pagesToFetch = [...new Set(pagesToFetch)].filter((page) => page >= 0).sort((a, b) => a - b);
|
||||
|
||||
return {
|
||||
direction: newDirection,
|
||||
pagesToFetch,
|
||||
thresholdDistance,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useElementSize, useMergedRef } from '@mantine/hooks';
|
||||
import clsx from 'clsx';
|
||||
import debounce from 'lodash/debounce';
|
||||
import throttle from 'lodash/throttle';
|
||||
import { AnimatePresence } from 'motion/react';
|
||||
import { useOverlayScrollbars } from 'overlayscrollbars-react';
|
||||
@@ -114,38 +113,23 @@ const VirtualizedGridList = React.memo(
|
||||
itemType,
|
||||
]);
|
||||
|
||||
const debouncedOnScrollEnd = useMemo(
|
||||
() =>
|
||||
onScrollEnd
|
||||
? debounce((scrollOffset: number, direction: 'down' | 'up') => {
|
||||
onScrollEnd(scrollOffset, direction);
|
||||
}, 100)
|
||||
: undefined,
|
||||
[onScrollEnd],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
debouncedOnScrollEnd?.cancel();
|
||||
};
|
||||
}, [debouncedOnScrollEnd]);
|
||||
|
||||
const handleOnScroll = useCallback(
|
||||
({ scrollDirection, scrollOffset }: ListOnScrollProps) => {
|
||||
onScroll?.(scrollOffset, scrollDirection === 'forward' ? 'down' : 'up');
|
||||
debouncedOnScrollEnd?.(scrollOffset, scrollDirection === 'forward' ? 'down' : 'up');
|
||||
onScrollEnd?.(scrollOffset, scrollDirection === 'forward' ? 'down' : 'up');
|
||||
},
|
||||
[onScroll, debouncedOnScrollEnd],
|
||||
[onScroll, onScrollEnd],
|
||||
);
|
||||
|
||||
const debouncedOnItemsRendered = useMemo(() => {
|
||||
return debounce((items: ListOnItemsRenderedProps) => {
|
||||
const handleOnItemsRendered = useCallback(
|
||||
(items: ListOnItemsRenderedProps) => {
|
||||
onRangeChanged?.({
|
||||
startIndex: items.visibleStartIndex * (tableMeta?.columnCount || 0),
|
||||
stopIndex: items.visibleStopIndex * (tableMeta?.columnCount || 0),
|
||||
});
|
||||
}, 50);
|
||||
}, [onRangeChanged, tableMeta?.columnCount]);
|
||||
},
|
||||
[onRangeChanged, tableMeta?.columnCount],
|
||||
);
|
||||
|
||||
if (!tableMeta) {
|
||||
return null;
|
||||
@@ -169,7 +153,7 @@ const VirtualizedGridList = React.memo(
|
||||
itemCount={itemData.tableMeta?.rowCount || 0}
|
||||
itemData={itemData}
|
||||
itemSize={itemData.tableMeta?.itemHeight || 0}
|
||||
onItemsRendered={debouncedOnItemsRendered}
|
||||
onItemsRendered={handleOnItemsRendered}
|
||||
onScroll={handleOnScroll}
|
||||
outerRef={outerRef}
|
||||
ref={ref}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { autoScrollForElements } from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/element';
|
||||
import { useMergedRef } from '@mantine/hooks';
|
||||
import clsx from 'clsx';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { AnimatePresence } from 'motion/react';
|
||||
import { useOverlayScrollbars } from 'overlayscrollbars-react';
|
||||
import React, {
|
||||
@@ -266,22 +265,20 @@ const VirtualizedTableGrid = React.memo(
|
||||
[pinnedLeftColumnCount, pinnedRowCount, CellComponent],
|
||||
);
|
||||
|
||||
const debouncedOnCellsRendered = useMemo(() => {
|
||||
return debounce(
|
||||
(items: {
|
||||
columnStartIndex: number;
|
||||
columnStopIndex: number;
|
||||
rowStartIndex: number;
|
||||
rowStopIndex: number;
|
||||
}) => {
|
||||
onRangeChanged?.({
|
||||
startIndex: items.rowStartIndex,
|
||||
stopIndex: items.rowStopIndex,
|
||||
});
|
||||
},
|
||||
45,
|
||||
);
|
||||
}, [onRangeChanged]);
|
||||
const handleOnCellsRendered = useCallback(
|
||||
(items: {
|
||||
columnStartIndex: number;
|
||||
columnStopIndex: number;
|
||||
rowStartIndex: number;
|
||||
rowStopIndex: number;
|
||||
}) => {
|
||||
onRangeChanged?.({
|
||||
startIndex: items.rowStartIndex,
|
||||
stopIndex: items.rowStopIndex,
|
||||
});
|
||||
},
|
||||
[onRangeChanged],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.itemTableContainer}>
|
||||
@@ -379,7 +376,7 @@ const VirtualizedTableGrid = React.memo(
|
||||
columnWidth={(index) => {
|
||||
return columnWidth(index + pinnedLeftColumnCount);
|
||||
}}
|
||||
onCellsRendered={debouncedOnCellsRendered}
|
||||
onCellsRendered={handleOnCellsRendered}
|
||||
rowCount={totalRowCount}
|
||||
rowHeight={(index, cellProps) => {
|
||||
return getRowHeight(index + pinnedRowCount, cellProps);
|
||||
|
||||
Reference in New Issue
Block a user