From 880516069d4773b9fa462c4407f8ddf7a6be5c04 Mon Sep 17 00:00:00 2001 From: Pedro Vieira Date: Tue, 9 Jun 2026 19:21:02 +0100 Subject: [PATCH] fix: preserve infinite list cache on component remount (fixes random sort reshuffling) (#2097) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: preserve infinite list cache on component remount When browsing with random sort, navigating to a detail view and coming back would reshuffle the list. This happens because the list component unmounts, losing its internal ref guard, and the reset effect re-fetches all pages — returning a new random order from the server. --- .../helpers/item-list-infinite-loader.ts | 36 +++++++++++++++++-- .../helpers/item-list-paginated-loader.ts | 8 +++-- src/shared/types/domain-types.ts | 10 +++--- 3 files changed, 45 insertions(+), 9 deletions(-) diff --git a/src/renderer/components/item-list/helpers/item-list-infinite-loader.ts b/src/renderer/components/item-list/helpers/item-list-infinite-loader.ts index 9bbfc1ce3..986ace5a5 100644 --- a/src/renderer/components/item-list/helpers/item-list-infinite-loader.ts +++ b/src/renderer/components/item-list/helpers/item-list-infinite-loader.ts @@ -13,7 +13,7 @@ import { useListContext } from '/@/renderer/context/list-context'; import { eventEmitter } from '/@/renderer/events/event-emitter'; import { UserFavoriteEventPayload, UserRatingEventPayload } from '/@/renderer/events/events'; import { getListRefreshMutationKey } from '/@/renderer/features/shared/components/list-refresh-button'; -import { LibraryItem } from '/@/shared/types/domain-types'; +import { LibraryItem, SortKeyRandom } from '/@/shared/types/domain-types'; export const getListQueryKeyName = (itemType: LibraryItem): string => { switch (itemType) { @@ -108,8 +108,19 @@ export const useItemListInfiniteLoader = ({ [serverId, itemType, query], ); + const isRandomSort = query?.sortBy === SortKeyRandom; + const fetchPage = useCallback( async (pageNumber: number) => { + if (isRandomSort) { + const existingData = + queryClient.getQueryData(dataQueryKey); + if (existingData?.pagesLoaded?.[pageNumber]) { + lastFetchedPageRef.current = Math.max(lastFetchedPageRef.current, pageNumber); + return; + } + } + const startIndex = pageNumber * itemsPerPage; const queryParams = { limit: itemsPerPage, @@ -118,6 +129,7 @@ export const useItemListInfiniteLoader = ({ }; const result = await queryClient.fetchQuery({ + gcTime: isRandomSort ? 1000 * 60 * 10 : 1000 * 15, queryFn: async ({ signal }) => { const result = await listQueryFn({ apiClientProps: { serverId, signal }, @@ -127,6 +139,7 @@ export const useItemListInfiniteLoader = ({ return result; }, queryKey: queryKeys[getListQueryKeyName(itemType)].list(serverId, queryParams), + staleTime: isRandomSort ? 1000 * 60 * 10 : 1000 * 15, }); // Update the query data with the fetched page @@ -154,13 +167,32 @@ export const useItemListInfiniteLoader = ({ // Track the last fetched page lastFetchedPageRef.current = Math.max(lastFetchedPageRef.current, pageNumber); }, - [itemsPerPage, query, queryClient, serverId, dataQueryKey, listQueryFn, itemType], + [ + itemsPerPage, + query, + queryClient, + serverId, + dataQueryKey, + listQueryFn, + itemType, + isRandomSort, + ], ); // Reset the loaded pages and refetch current page when the query changes useEffect(() => { const currentDataQueryKey = JSON.stringify(dataQueryKey); + if (isRandomSort) { + const existingData = queryClient.getQueryData( + dataQueryKey, + ); + if (existingData?.dataMap && existingData.dataMap.size > 0) { + previousDataQueryKeyRef.current = currentDataQueryKey; + return; + } + } + if (previousDataQueryKeyRef.current === currentDataQueryKey || isRefetchingRef.current) { return; } diff --git a/src/renderer/components/item-list/helpers/item-list-paginated-loader.ts b/src/renderer/components/item-list/helpers/item-list-paginated-loader.ts index f3e0f2100..a5f23670e 100644 --- a/src/renderer/components/item-list/helpers/item-list-paginated-loader.ts +++ b/src/renderer/components/item-list/helpers/item-list-paginated-loader.ts @@ -12,7 +12,7 @@ import { useListContext } from '/@/renderer/context/list-context'; import { eventEmitter } from '/@/renderer/events/event-emitter'; import { UserFavoriteEventPayload, UserRatingEventPayload } from '/@/renderer/events/events'; import { getListRefreshMutationKey } from '/@/renderer/features/shared/components/list-refresh-button'; -import { LibraryItem } from '/@/shared/types/domain-types'; +import { LibraryItem, SortKeyRandom } from '/@/shared/types/domain-types'; const getQueryKeyName = (itemType: LibraryItem): string => { switch (itemType) { @@ -76,6 +76,8 @@ export const useItemListPaginatedLoader = ({ const fetchRange = getFetchRange(currentPage, itemsPerPage); const startIndex = fetchRange.startIndex; + const isRandomSort = query?.sortBy === SortKeyRandom; + const queryParams = useMemo( () => ({ limit: itemsPerPage, @@ -86,7 +88,7 @@ export const useItemListPaginatedLoader = ({ ); const { data } = useQuery({ - gcTime: 1000 * 15, + gcTime: isRandomSort ? 1000 * 60 * 10 : 1000 * 15, placeholderData: { items: getInitialData(itemsPerPage) }, queryFn: async ({ signal }) => { const result = await listQueryFn({ @@ -97,7 +99,7 @@ export const useItemListPaginatedLoader = ({ return result; }, queryKey: queryKeys[getQueryKeyName(itemType)].list(serverId, queryParams), - staleTime: 1000 * 15, + staleTime: isRandomSort ? 1000 * 60 * 10 : 1000 * 15, }); const refreshMutation = useMutation({ diff --git a/src/shared/types/domain-types.ts b/src/shared/types/domain-types.ts index 2aeaaf579..30e8446e9 100644 --- a/src/shared/types/domain-types.ts +++ b/src/shared/types/domain-types.ts @@ -465,6 +465,8 @@ export const tagListSortMap: TagListSortMap = { }, }; +export const SortKeyRandom = 'random'; + export enum AlbumListSort { ALBUM_ARTIST = 'albumArtist', ARTIST = 'artist', @@ -476,7 +478,7 @@ export enum AlbumListSort { ID = 'id', NAME = 'name', PLAY_COUNT = 'playCount', - RANDOM = 'random', + RANDOM = SortKeyRandom, RATING = 'rating', RECENTLY_ADDED = 'recentlyAdded', RECENTLY_PLAYED = 'recentlyPlayed', @@ -598,7 +600,7 @@ export enum SongListSort { ID = 'id', NAME = 'name', PLAY_COUNT = 'playCount', - RANDOM = 'random', + RANDOM = SortKeyRandom, RATING = 'rating', RECENTLY_ADDED = 'recentlyAdded', RECENTLY_PLAYED = 'recentlyPlayed', @@ -725,7 +727,7 @@ export enum AlbumArtistListSort { FAVORITED = 'favorited', NAME = 'name', PLAY_COUNT = 'playCount', - RANDOM = 'random', + RANDOM = SortKeyRandom, RATING = 'rating', RECENTLY_ADDED = 'recentlyAdded', RELEASE_DATE = 'releaseDate', @@ -814,7 +816,7 @@ export enum ArtistListSort { FAVORITED = 'favorited', NAME = 'name', PLAY_COUNT = 'playCount', - RANDOM = 'random', + RANDOM = SortKeyRandom, RATING = 'rating', RECENTLY_ADDED = 'recentlyAdded', RELEASE_DATE = 'releaseDate',