diff --git a/apps/client/src/features/base/components/base-table.tsx b/apps/client/src/features/base/components/base-table.tsx index 8de15dff1..5da2c94de 100644 --- a/apps/client/src/features/base/components/base-table.tsx +++ b/apps/client/src/features/base/components/base-table.tsx @@ -26,7 +26,7 @@ type BaseTableProps = { dropPosition: "above" | "below", ) => void; persistViewConfig: () => void; - scrollportRef: React.RefObject; + scrollportEl: HTMLDivElement | null; stickyBandPrelude?: React.ReactNode; }; @@ -44,7 +44,7 @@ export function BaseTable({ onColumnReorder, onResizeEnd, onRowReorder, - scrollportRef, + scrollportEl, stickyBandPrelude, }: BaseTableProps) { return ( @@ -60,7 +60,7 @@ export function BaseTable({ hasNextPage={hasNextPage} isFetchingNextPage={isFetchingNextPage} onFetchNextPage={onFetchNextPage} - scrollElement={embedded ? window : scrollportRef.current} + scrollElement={embedded ? window : scrollportEl} stickyBandPrelude={stickyBandPrelude ?? null} /> ); diff --git a/apps/client/src/features/base/components/base-view.tsx b/apps/client/src/features/base/components/base-view.tsx index 66bd950d1..54328ae84 100644 --- a/apps/client/src/features/base/components/base-view.tsx +++ b/apps/client/src/features/base/components/base-view.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useRef } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { Text, Stack } from "@mantine/core"; import { useAtom } from "jotai"; import { IconDatabase } from "@tabler/icons-react"; @@ -159,7 +159,15 @@ export function BaseView({ pageId, embedded }: BaseViewProps) { clearSelection(); }, [pageId, activeView?.id, clearSelection]); - const scrollportRef = useRef(null); + // Track the scrollport element in state (not a ref) so the virtualizer's + // `_willUpdate` re-runs when the div attaches on first mount. Reading + // `scrollportRef.current` during render would always be null on the + // render that mounts the div, and no subsequent render is guaranteed — + // particularly after a filter change, where the scrollport remounts via + // the `rowsLoading` skeleton path. The virtualizer would then sit on + // `scrollElement=null`, render zero items, and only recover when + // something else forced a re-render (e.g. switching views). + const [scrollportEl, setScrollportEl] = useState(null); const rows = useMemo(() => { const flat = flattenRows(rowsData); @@ -369,7 +377,7 @@ export function BaseView({ pageId, embedded }: BaseViewProps) { onRowReorder={handleRowReorder} onCardClick={handleCardClick} persistViewConfig={persistViewConfig} - scrollportRef={scrollportRef} + scrollportEl={scrollportEl} stickyBandPrelude={ <> {banner} @@ -397,7 +405,7 @@ export function BaseView({ pageId, embedded }: BaseViewProps) { > {banner} {toolbar} -
+
diff --git a/apps/client/src/features/base/components/views/view-renderer.tsx b/apps/client/src/features/base/components/views/view-renderer.tsx index c464fd902..8eb14af1f 100644 --- a/apps/client/src/features/base/components/views/view-renderer.tsx +++ b/apps/client/src/features/base/components/views/view-renderer.tsx @@ -28,7 +28,7 @@ type ViewRendererProps = { ) => void; onCardClick: (rowId: string) => void; persistViewConfig: () => void; - scrollportRef: React.RefObject; + scrollportEl: HTMLDivElement | null; stickyBandPrelude?: React.ReactNode; };