fix(favorites): sync empty-state on cache hits to avoid blank view

Switching Favorites<->Watchlist (props swap in place, no remount) left the
page blank with only the tab buttons: the empty-state was written as a side
effect inside the queryFn, which React Query skips on cache hits, and a reset
effect then cleared it — so on a cache-served switch nothing repopulated it,
every list was hidden by hideIfEmpty, and the empty message never rendered.

Each list now reports emptiness once its query settles (incl. cache hits) via
a new optional onEmptyStateChange callback on InfiniteScrollingCollectionList
(mobile + TV), reporting null while loading so a switch never flashes a stale
state. The parent derives the aggregate empty-state from that (null = not
settled yet, so the message stays hidden during a switch).

The reset effect is removed entirely, not kept: React runs child effects
before parent effects within a commit, so a parent reset setEmptyState(false)
would clobber the children's reported values on the same render. Dropping it
and using the per-list callbacks (tri-state null/true/false) is what makes the
switch — including a cache-served one — resolve to the correct state. Applies
to both mobile and TV Favorites.
This commit is contained in:
Gauvain
2026-07-01 22:31:15 +02:00
parent 17a591cd3c
commit 0cac1f8779
4 changed files with 95 additions and 69 deletions

View File

@@ -6,7 +6,7 @@ import {
useInfiniteQuery,
} from "@tanstack/react-query";
import { useSegments } from "expo-router";
import { useCallback, useMemo, useRef } from "react";
import { useCallback, useEffect, useMemo, useRef } from "react";
import { useTranslation } from "react-i18next";
import {
ActivityIndicator,
@@ -42,6 +42,13 @@ interface Props extends ViewProps {
isFirstSection?: boolean;
onItemFocus?: (item: BaseItemDto) => void;
parentId?: string;
/**
* Reports emptiness whenever the query settles (incl. cache hits):
* `null` while loading (unknown), otherwise whether the list is empty.
* Lets a parent derive an aggregate empty-state reactively instead of via a
* queryFn side effect, which React Query skips when it serves cache.
*/
onEmptyStateChange?: (isEmpty: boolean | null) => void;
}
type Typography = ReturnType<typeof useScaledTVTypography>;
@@ -123,6 +130,7 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
isFirstSection = false,
onItemFocus,
parentId,
onEmptyStateChange,
...props
}) => {
const typography = useScaledTVTypography();
@@ -182,6 +190,14 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
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.
const onEmptyStateChangeRef = useRef(onEmptyStateChange);
onEmptyStateChangeRef.current = onEmptyStateChange;
useEffect(() => {
onEmptyStateChangeRef.current?.(isLoading ? null : allItems.length === 0);
}, [isLoading, allItems.length]);
const itemWidth =
orientation === "horizontal" ? posterSizes.episode : posterSizes.poster;