fix(ui): stabilize infinite-stream callbacks + suppress empty-state flash

- useInfiniteStream: wrap fetchNextPage and refresh in useCallback so
  InfiniteScrollArea's IntersectionObserver does not re-subscribe on
  every parent render.
- InfiniteScrollArea: do not render 'End of stream' until at least one
  item has loaded and the initial query has settled (was flashing on
  mount before first fetch).
- AgentHealth: pass isLoading + hasItems to both InfiniteScrollArea
  wrappers.
This commit is contained in:
hsiegeln
2026-04-17 12:52:39 +02:00
parent fb7d6db375
commit 7f233460aa
3 changed files with 22 additions and 6 deletions

View File

@@ -6,6 +6,8 @@ export interface InfiniteScrollAreaProps {
onTopVisibilityChange?: (atTop: boolean) => void;
isFetchingNextPage: boolean;
hasNextPage: boolean;
isLoading?: boolean;
hasItems?: boolean;
maxHeight?: number | string;
children: ReactNode;
/** Optional caller-owned scroll container ref (e.g. for scroll-to-top on refresh). */
@@ -18,6 +20,8 @@ export function InfiniteScrollArea({
onTopVisibilityChange,
isFetchingNextPage,
hasNextPage,
isLoading = false,
hasItems = true,
maxHeight = 360,
children,
scrollRef,
@@ -68,7 +72,9 @@ export function InfiniteScrollArea({
{children}
<div ref={bottomSentinel} className={styles.sentinel} aria-hidden="true" />
{isFetchingNextPage && <div className={styles.loadingMore}>Loading more</div>}
{!hasNextPage && <div className={styles.endOfStream}>End of stream</div>}
{!hasNextPage && !isLoading && hasItems && (
<div className={styles.endOfStream}>End of stream</div>
)}
</div>
);
}