diff --git a/client/src/app/components/InfiniteScroller/InfiniteScroller.tsx b/client/src/app/components/InfiniteScroller/InfiniteScroller.tsx index 9e4ddfbc6..eb990f39a 100644 --- a/client/src/app/components/InfiniteScroller/InfiniteScroller.tsx +++ b/client/src/app/components/InfiniteScroller/InfiniteScroller.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, useEffect, useRef } from "react"; +import React, { ReactNode, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useVisibilityTracker } from "./useVisibilityTracker"; import "./InfiniteScroller.css"; @@ -12,6 +12,7 @@ export interface InfiniteScrollerProps { hasMore: boolean; // number of items currently displayed/known itemCount: number; + pageSize: number; } export const InfiniteScroller = ({ @@ -19,40 +20,38 @@ export const InfiniteScroller = ({ fetchMore, hasMore, itemCount, + pageSize, }: InfiniteScrollerProps) => { const { t } = useTranslation(); - // Track how many items were known at time of triggering the fetch. - // This allows to detect edge case when second(or more) fetchMore() is triggered before - // IntersectionObserver is able to detect out-of-view event. - // Initializing with zero ensures that the effect will be triggered immediately - // (parent is expected to display empty state until some items are available). - const itemCountRef = useRef(0); + const [readyForFetch, setReadyForFetch] = useState(false); const { visible: isSentinelVisible, nodeRef: sentinelRef } = useVisibilityTracker({ enable: hasMore, }); + useEffect(() => { + // enable or clear the flag depending on the sentinel visibility + setReadyForFetch(!!isSentinelVisible); + }, [isSentinelVisible]); - useEffect( - () => { - if ( - isSentinelVisible && - itemCountRef.current !== itemCount && - fetchMore() // fetch may be blocked if background refresh is in progress (or other manual fetch) - ) { - // fetchMore call was triggered (it may fail but will be subject to React Query retry policy) - itemCountRef.current = itemCount; - } - }, + useEffect(() => { + if (readyForFetch) { + // clear the flag if fetch request is accepted + setReadyForFetch(!fetchMore()); + } // reference to fetchMore() changes based on query state and ensures that the effect is triggered in the right moment // i.e. after fetch triggered by the previous fetchMore() call finished - [isSentinelVisible, fetchMore, itemCount] - ); + }, [fetchMore, readyForFetch]); return (
{children} {hasMore && ( -
+
{t("message.loadingTripleDot")}
)} diff --git a/client/src/app/components/InfiniteScroller/useVisibilityTracker.tsx b/client/src/app/components/InfiniteScroller/useVisibilityTracker.tsx index c6f8135b2..c30b42d7d 100644 --- a/client/src/app/components/InfiniteScroller/useVisibilityTracker.tsx +++ b/client/src/app/components/InfiniteScroller/useVisibilityTracker.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState, useCallback } from "react"; export function useVisibilityTracker({ enable }: { enable: boolean }) { const nodeRef = useRef(null); - const [visible, setVisible] = useState(false); + const [visible, setVisible] = useState(false); const node = nodeRef.current; // state is set from IntersectionObserver callbacks which may not align with React lifecycle @@ -22,14 +22,19 @@ export function useVisibilityTracker({ enable }: { enable: boolean }) { }, []); useEffect(() => { + if (enable && !node) { + // use falsy value different than initial value - state change will trigger render() + // otherwise we need to wait for the next render() to read node ref + setVisibleSafe(undefined); + return undefined; + } + if (!enable || !node) { return undefined; } // Observer with default options - the whole view port used. // Note that if root element is used then it needs to be the ancestor of the target. - // In case of infinite scroller the target is always within the (scrollable!)parent - // even if the node is technically hidden from the user. // https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#root const observer = new IntersectionObserver( (entries: IntersectionObserverEntry[]) => diff --git a/client/src/app/components/task-manager/TaskManagerDrawer.tsx b/client/src/app/components/task-manager/TaskManagerDrawer.tsx index 4a1faf69f..10824bc16 100644 --- a/client/src/app/components/task-manager/TaskManagerDrawer.tsx +++ b/client/src/app/components/task-manager/TaskManagerDrawer.tsx @@ -66,7 +66,8 @@ interface TaskManagerDrawerProps { export const TaskManagerDrawer: React.FC = forwardRef( (_props, ref) => { const { isExpanded, setIsExpanded, queuedCount } = useTaskManagerContext(); - const { tasks, hasNextPage, fetchNextPage } = useTaskManagerData(); + const { tasks, hasNextPage, fetchNextPage, pageSize } = + useTaskManagerData(); const [expandedItems, setExpandedItems] = useState([]); const [taskWithExpandedActions, setTaskWithExpandedAction] = useState< @@ -106,6 +107,7 @@ export const TaskManagerDrawer: React.FC = forwardRef( fetchMore={fetchNextPage} hasMore={hasNextPage} itemCount={tasks?.length ?? 0} + pageSize={pageSize} > {tasks.map((task) => ( @@ -282,6 +284,7 @@ const useTaskManagerData = () => { [data] ); + // note that the callback will change when query fetching state changes const fetchMore = useCallback(() => { // forced fetch is not allowed when background fetch or other forced fetch is in progress if (!isFetching && !isFetchingNextPage) { @@ -297,6 +300,6 @@ const useTaskManagerData = () => { isFetching, hasNextPage, fetchNextPage: fetchMore, - isReadyToFetch: !isFetching && !isFetchingNextPage, + pageSize: PAGE_SIZE, }; };