From ee3c5ce9d9aafeea1f28d852a3643194b92619bf Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Mon, 25 May 2026 15:53:28 +0100 Subject: [PATCH] fix(bases): render filtered rows on first paint in standalone view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Track the scrollport element in state instead of reading `scrollportRef.current` during render. The ref was always null on the render that mounts the `.tableScrollport` div, so `useVirtualizer`'s `_willUpdate` saw `scrollElement=null`, skipped observer attachment, and `calculateRange` returned null — rendering zero rows even though the `/rows` response was already in the React-Query cache. The bug surfaced after a filter change (the `rowsLoading` skeleton path remounts the scrollport, and no follow-on render is guaranteed once `/rows` settles) but not on first base load (slower side queries forced an extra render that coincidentally re-bound the virtualizer). Switching views also masked it: the re-render triggered `_willUpdate` with a now- populated ref. Using a callback-ref-backed `useState` triggers a render the moment the div attaches, so the virtualizer picks it up on the next pass — no view-switch workaround needed. --- .../features/base/components/base-table.tsx | 6 +++--- .../src/features/base/components/base-view.tsx | 18 +++++++++++++----- .../base/components/views/view-renderer.tsx | 2 +- 3 files changed, 17 insertions(+), 9 deletions(-) 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; };