From 28bf70c0ac3d23dd8c25d73c57cfadbba8e8c703 Mon Sep 17 00:00:00 2001 From: Gauvain Date: Sat, 4 Jul 2026 18:22:16 +0200 Subject: [PATCH] fix(favorites): report unknown empty state on query errors A first fetch that fails with no cached data settles with isLoading false and zero items, which reported the section as empty and let the screen show the no-data view on transport/auth failures. Report null (unknown) instead so the aggregate empty state stays hidden. --- .../home/InfiniteScrollingCollectionList.tsx | 12 +++-- .../InfiniteScrollingCollectionList.tv.tsx | 53 +++++++++++-------- 2 files changed, 39 insertions(+), 26 deletions(-) diff --git a/components/home/InfiniteScrollingCollectionList.tsx b/components/home/InfiniteScrollingCollectionList.tsx index e31b69cc..bac967fe 100644 --- a/components/home/InfiniteScrollingCollectionList.tsx +++ b/components/home/InfiniteScrollingCollectionList.tsx @@ -60,6 +60,7 @@ export const InfiniteScrollingCollectionList: React.FC = ({ const { data, isLoading, + isError, isFetchingNextPage, hasNextPage, fetchNextPage, @@ -111,13 +112,16 @@ export const InfiniteScrollingCollectionList: React.FC = ({ return deduped; }, [data]); - // Report emptiness on every settle (incl. cache hits). Callback held in a ref - // so an inline parent callback doesn't retrigger the effect each render. + // Report emptiness on every settle (incl. cache hits). Errors report null + // (unknown) so a failed fetch never reads as "no content". Callback held in + // a ref so an inline parent callback doesn't retrigger the effect each render. const onEmptyStateChangeRef = useRef(onEmptyStateChange); onEmptyStateChangeRef.current = onEmptyStateChange; useEffect(() => { - onEmptyStateChangeRef.current?.(isLoading ? null : allItems.length === 0); - }, [isLoading, allItems.length]); + onEmptyStateChangeRef.current?.( + isLoading || isError ? null : allItems.length === 0, + ); + }, [isLoading, isError, allItems.length]); const snapOffsets = useMemo(() => { const itemWidth = orientation === "horizontal" ? 184 : 120; // w-44 (176px) + mr-2 (8px) or w-28 (112px) + mr-2 (8px) diff --git a/components/home/InfiniteScrollingCollectionList.tv.tsx b/components/home/InfiniteScrollingCollectionList.tv.tsx index 1e27fae8..db883ca3 100644 --- a/components/home/InfiniteScrollingCollectionList.tv.tsx +++ b/components/home/InfiniteScrollingCollectionList.tv.tsx @@ -153,24 +153,30 @@ export const InfiniteScrollingCollectionList: React.FC = ({ [onItemFocus], ); - const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } = - useInfiniteQuery({ - queryKey: queryKey, - queryFn: ({ pageParam = 0, ...context }) => - queryFn({ ...context, queryKey, pageParam }), - getNextPageParam: (lastPage, allPages) => { - if (lastPage.length < effectivePageSize) { - return undefined; - } - return allPages.reduce((acc, page) => acc + page.length, 0); - }, - initialPageParam: 0, - staleTime: 60 * 1000, - refetchInterval: 60 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: true, - enabled, - }); + const { + data, + isLoading, + isError, + isFetchingNextPage, + hasNextPage, + fetchNextPage, + } = useInfiniteQuery({ + queryKey: queryKey, + queryFn: ({ pageParam = 0, ...context }) => + queryFn({ ...context, queryKey, pageParam }), + getNextPageParam: (lastPage, allPages) => { + if (lastPage.length < effectivePageSize) { + return undefined; + } + return allPages.reduce((acc, page) => acc + page.length, 0); + }, + initialPageParam: 0, + staleTime: 60 * 1000, + refetchInterval: 60 * 1000, + refetchOnWindowFocus: false, + refetchOnReconnect: true, + enabled, + }); const { t } = useTranslation(); @@ -190,13 +196,16 @@ export const InfiniteScrollingCollectionList: React.FC = ({ return deduped; }, [data]); - // Report emptiness on every settle (incl. cache hits). Callback held in a ref - // so an inline parent callback doesn't retrigger the effect each render. + // Report emptiness on every settle (incl. cache hits). Errors report null + // (unknown) so a failed fetch never reads as "no content". Callback held in + // a ref so an inline parent callback doesn't retrigger the effect each render. const onEmptyStateChangeRef = useRef(onEmptyStateChange); onEmptyStateChangeRef.current = onEmptyStateChange; useEffect(() => { - onEmptyStateChangeRef.current?.(isLoading ? null : allItems.length === 0); - }, [isLoading, allItems.length]); + onEmptyStateChangeRef.current?.( + isLoading || isError ? null : allItems.length === 0, + ); + }, [isLoading, isError, allItems.length]); const itemWidth = orientation === "horizontal" ? posterSizes.episode : posterSizes.poster;