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;
|
onTopVisibilityChange?: (atTop: boolean) => void;
|
||||||
isFetchingNextPage: boolean;
|
isFetchingNextPage: boolean;
|
||||||
hasNextPage: boolean;
|
hasNextPage: boolean;
|
||||||
|
isLoading?: boolean;
|
||||||
|
hasItems?: boolean;
|
||||||
maxHeight?: number | string;
|
maxHeight?: number | string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
/** Optional caller-owned scroll container ref (e.g. for scroll-to-top on refresh). */
|
/** Optional caller-owned scroll container ref (e.g. for scroll-to-top on refresh). */
|
||||||
@@ -18,6 +20,8 @@ export function InfiniteScrollArea({
|
|||||||
onTopVisibilityChange,
|
onTopVisibilityChange,
|
||||||
isFetchingNextPage,
|
isFetchingNextPage,
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
|
isLoading = false,
|
||||||
|
hasItems = true,
|
||||||
maxHeight = 360,
|
maxHeight = 360,
|
||||||
children,
|
children,
|
||||||
scrollRef,
|
scrollRef,
|
||||||
@@ -68,7 +72,9 @@ export function InfiniteScrollArea({
|
|||||||
{children}
|
{children}
|
||||||
<div ref={bottomSentinel} className={styles.sentinel} aria-hidden="true" />
|
<div ref={bottomSentinel} className={styles.sentinel} aria-hidden="true" />
|
||||||
{isFetchingNextPage && <div className={styles.loadingMore}>Loading more…</div>}
|
{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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query';
|
import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
export interface StreamPage<T> {
|
export interface StreamPage<T> {
|
||||||
data: T[];
|
data: T[];
|
||||||
@@ -52,14 +52,20 @@ export function useInfiniteStream<T>(args: UseInfiniteStreamArgs<T>): UseInfinit
|
|||||||
[query.data],
|
[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 {
|
return {
|
||||||
items,
|
items,
|
||||||
fetchNextPage: () => {
|
fetchNextPage,
|
||||||
if (query.hasNextPage && !query.isFetchingNextPage) query.fetchNextPage();
|
|
||||||
},
|
|
||||||
hasNextPage: !!query.hasNextPage,
|
hasNextPage: !!query.hasNextPage,
|
||||||
isFetchingNextPage: query.isFetchingNextPage,
|
isFetchingNextPage: query.isFetchingNextPage,
|
||||||
isLoading: query.isLoading,
|
isLoading: query.isLoading,
|
||||||
refresh: () => queryClient.invalidateQueries({ queryKey: [...queryKey] }),
|
refresh,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -957,6 +957,8 @@ export default function AgentHealth() {
|
|||||||
onTopVisibilityChange={setIsLogAtTop}
|
onTopVisibilityChange={setIsLogAtTop}
|
||||||
isFetchingNextPage={logStream.isFetchingNextPage}
|
isFetchingNextPage={logStream.isFetchingNextPage}
|
||||||
hasNextPage={logStream.hasNextPage}
|
hasNextPage={logStream.hasNextPage}
|
||||||
|
isLoading={logStream.isLoading}
|
||||||
|
hasItems={logStream.items.length > 0}
|
||||||
maxHeight={360}
|
maxHeight={360}
|
||||||
>
|
>
|
||||||
{filteredLogs.length > 0 ? (
|
{filteredLogs.length > 0 ? (
|
||||||
@@ -993,6 +995,8 @@ export default function AgentHealth() {
|
|||||||
onTopVisibilityChange={setIsTimelineAtTop}
|
onTopVisibilityChange={setIsTimelineAtTop}
|
||||||
isFetchingNextPage={eventStream.isFetchingNextPage}
|
isFetchingNextPage={eventStream.isFetchingNextPage}
|
||||||
hasNextPage={eventStream.hasNextPage}
|
hasNextPage={eventStream.hasNextPage}
|
||||||
|
isLoading={eventStream.isLoading}
|
||||||
|
hasItems={eventStream.items.length > 0}
|
||||||
maxHeight={360}
|
maxHeight={360}
|
||||||
>
|
>
|
||||||
{feedEvents.length > 0 ? (
|
{feedEvents.length > 0 ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user