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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useMemo } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
export interface StreamPage<T> {
|
||||
data: T[];
|
||||
@@ -52,14 +52,20 @@ export function useInfiniteStream<T>(args: UseInfiniteStreamArgs<T>): UseInfinit
|
||||
[query.data],
|
||||
);
|
||||
|
||||
const fetchNextPage = useCallback(() => {
|
||||
if (query.hasNextPage && !query.isFetchingNextPage) query.fetchNextPage();
|
||||
}, [query.hasNextPage, query.isFetchingNextPage, query.fetchNextPage]);
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
queryClient.invalidateQueries({ queryKey: [...queryKey] });
|
||||
}, [queryClient, queryKey]);
|
||||
|
||||
return {
|
||||
items,
|
||||
fetchNextPage: () => {
|
||||
if (query.hasNextPage && !query.isFetchingNextPage) query.fetchNextPage();
|
||||
},
|
||||
fetchNextPage,
|
||||
hasNextPage: !!query.hasNextPage,
|
||||
isFetchingNextPage: query.isFetchingNextPage,
|
||||
isLoading: query.isLoading,
|
||||
refresh: () => queryClient.invalidateQueries({ queryKey: [...queryKey] }),
|
||||
refresh,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -957,6 +957,8 @@ export default function AgentHealth() {
|
||||
onTopVisibilityChange={setIsLogAtTop}
|
||||
isFetchingNextPage={logStream.isFetchingNextPage}
|
||||
hasNextPage={logStream.hasNextPage}
|
||||
isLoading={logStream.isLoading}
|
||||
hasItems={logStream.items.length > 0}
|
||||
maxHeight={360}
|
||||
>
|
||||
{filteredLogs.length > 0 ? (
|
||||
@@ -993,6 +995,8 @@ export default function AgentHealth() {
|
||||
onTopVisibilityChange={setIsTimelineAtTop}
|
||||
isFetchingNextPage={eventStream.isFetchingNextPage}
|
||||
hasNextPage={eventStream.hasNextPage}
|
||||
isLoading={eventStream.isLoading}
|
||||
hasItems={eventStream.items.length > 0}
|
||||
maxHeight={360}
|
||||
>
|
||||
{feedEvents.length > 0 ? (
|
||||
|
||||
Reference in New Issue
Block a user