refactor item list to use accessor function

This commit is contained in:
jeffvli
2026-01-16 16:34:31 -08:00
parent e2b20eb89b
commit d1aed5007f
39 changed files with 528 additions and 324 deletions
@@ -32,6 +32,13 @@ export const getListQueryKeyName = (itemType: LibraryItem): string => {
}
};
type InfiniteLoaderCacheData = {
dataMap: Map<number, unknown>;
idToIndexMap: Map<string, number>;
pagesLoaded: Record<string, boolean>;
version: number;
};
interface UseItemListInfiniteLoaderProps {
eventKey: string;
fetchThreshold?: number;
@@ -43,10 +50,12 @@ interface UseItemListInfiniteLoaderProps {
serverId: string;
}
function getInitialData(itemCount: number) {
function getInitialData(): InfiniteLoaderCacheData {
return {
data: Array.from({ length: itemCount }, () => undefined),
dataMap: new Map(),
idToIndexMap: new Map(),
pagesLoaded: {},
version: 0,
};
}
@@ -118,28 +127,27 @@ export const useItemListInfiniteLoader = ({
queryKey: queryKeys[getListQueryKeyName(itemType)].list(serverId, queryParams),
});
const endIndex = startIndex + itemsPerPage;
// Update the query data with the fetched page
queryClient.setQueryData(
dataQueryKey,
(oldData: { data: unknown[]; pagesLoaded: Record<string, boolean> }) => {
const newData = [
...oldData.data.slice(0, startIndex),
...result.items,
...oldData.data.slice(endIndex),
];
const newPagesLoaded = {
...oldData.pagesLoaded,
[pageNumber]: true,
};
queryClient.setQueryData(dataQueryKey, (oldData: InfiniteLoaderCacheData) => {
const nextDataMap = new Map(oldData.dataMap);
const nextIdToIndexMap = new Map(oldData.idToIndexMap);
return {
data: newData,
pagesLoaded: newPagesLoaded,
};
},
);
result.items.forEach((item, offset) => {
const index = startIndex + offset;
nextDataMap.set(index, item);
if (item && typeof item === 'object' && 'id' in (item as any)) {
const id = String((item as any).id);
nextIdToIndexMap.set(id, index);
}
});
return {
dataMap: nextDataMap,
idToIndexMap: nextIdToIndexMap,
pagesLoaded: { ...oldData.pagesLoaded, [pageNumber]: true },
version: oldData.version + 1,
};
});
// Track the last fetched page
lastFetchedPageRef.current = Math.max(lastFetchedPageRef.current, pageNumber);
@@ -179,7 +187,10 @@ export const useItemListInfiniteLoader = ({
if (!oldData) return oldData;
return {
...oldData,
dataMap: new Map(),
idToIndexMap: new Map(),
pagesLoaded: {},
version: (oldData?.version ?? 0) + 1,
};
});
@@ -211,11 +222,11 @@ export const useItemListInfiniteLoader = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dataQueryKey, queryClient, fetchPage, itemsPerPage]);
const { data } = useQuery<{ data: unknown[]; pagesLoaded: Record<string, boolean> }>({
const { data } = useQuery<InfiniteLoaderCacheData>({
enabled: false,
initialData: getInitialData(totalItemCount),
initialData: getInitialData(),
queryFn: () => {
return getInitialData(totalItemCount);
return getInitialData();
},
queryKey: dataQueryKey,
});
@@ -233,7 +244,7 @@ export const useItemListInfiniteLoader = ({
const pageNumber = Math.floor(range.startIndex / itemsPerPage);
const currentData = queryClient.getQueryData<{
data: unknown[];
dataMap: Map<number, unknown>;
pagesLoaded: Record<string, boolean>;
}>(dataQueryKey);
@@ -289,18 +300,20 @@ export const useItemListInfiniteLoader = ({
// Reset the infinite list data
const currentData = queryClient.getQueryData<{
data: unknown[];
dataMap: Map<number, unknown>;
pagesLoaded: Record<string, boolean>;
}>(dataQueryKey);
if (force || currentData) {
// Reset data to initial state and clear all loaded pages
await queryClient.setQueryData(dataQueryKey, (oldData: any) => {
if (!oldData) return getInitialData(totalItemCount);
if (!oldData) return getInitialData();
return {
...oldData,
data: Array.from({ length: totalItemCount }, () => undefined),
dataMap: new Map(),
idToIndexMap: new Map(),
pagesLoaded: {},
version: (oldData?.version ?? 0) + 1,
};
});
lastFetchedPageRef.current = -1;
@@ -336,28 +349,23 @@ export const useItemListInfiniteLoader = ({
const updateItems = useCallback(
(indexes: number[], value: object) => {
queryClient.setQueryData(
dataQueryKey,
(prev: { data: unknown[]; pagesLoaded: Record<string, boolean> }) => {
return {
...prev,
data: prev.data.map((item: any, index) => {
if (!item) {
return item;
}
queryClient.setQueryData(dataQueryKey, (prev: InfiniteLoaderCacheData) => {
const nextDataMap = new Map(prev.dataMap);
if (!indexes.includes(index)) {
return item;
}
indexes.forEach((index) => {
const existing = nextDataMap.get(index);
if (!existing || typeof existing !== 'object') {
return;
}
nextDataMap.set(index, { ...(existing as any), ...(value as any) });
});
return {
...item,
...value,
};
}),
};
},
);
return {
...prev,
dataMap: nextDataMap,
version: prev.version + 1,
};
});
},
[queryClient, dataQueryKey],
);
@@ -384,16 +392,9 @@ export const useItemListInfiniteLoader = ({
return;
}
const idToIndexMap = data.data
.filter(Boolean)
.reduce((acc: Record<string, number>, item: any, index: number) => {
acc[item.id] = index;
return acc;
}, {});
const dataIndexes = payload.id
.map((id: string) => idToIndexMap[id])
.filter((idx) => idx !== undefined);
.map((id: string) => (data as any).idToIndexMap?.get(id))
.filter((idx): idx is number => typeof idx === 'number');
if (dataIndexes.length === 0) {
return;
@@ -407,16 +408,9 @@ export const useItemListInfiniteLoader = ({
return;
}
const idToIndexMap = data.data
.filter(Boolean)
.reduce((acc: Record<string, number>, item: any, index: number) => {
acc[item.id] = index;
return acc;
}, {});
const dataIndexes = payload.id
.map((id: string) => idToIndexMap[id])
.filter((idx) => idx !== undefined);
.map((id: string) => (data as any).idToIndexMap?.get(id))
.filter((idx): idx is number => typeof idx === 'number');
if (dataIndexes.length === 0) {
return;
@@ -434,7 +428,40 @@ export const useItemListInfiniteLoader = ({
};
}, [data, eventKey, itemType, serverId, updateItems]);
return { data: data.data, onRangeChanged, refresh, updateItems };
const itemCount = totalItemCount ?? 0;
const getItem = useCallback(
(index: number) => {
return (data as any).dataMap?.get(index);
},
[data],
);
const getItemIndex = useCallback(
(id: string) => {
return (data as any).idToIndexMap?.get(id);
},
[data],
);
const loadedItems = useMemo(() => {
const map: Map<number, unknown> | undefined = (data as any).dataMap;
if (!map || map.size === 0) return [];
return Array.from(map.entries())
.sort(([a], [b]) => a - b)
.map(([, v]) => v);
}, [data]);
return {
dataVersion: (data as any).version ?? 0,
getItem,
getItemIndex,
itemCount,
loadedItems,
onRangeChanged,
refresh,
updateItems,
};
};
export const parseListCountQuery = (query: any) => {