Skip to content

🐛 fix: VirtualizedTable query lifecycle + errorState/headerContent/scroll container - #666

Merged
futjesus merged 9 commits into
mainfrom
feat/virtualized-table-overhaul
Jul 21, 2026
Merged

🐛 fix: VirtualizedTable query lifecycle + errorState/headerContent/scroll container#666
futjesus merged 9 commits into
mainfrom
feat/virtualized-table-overhaul

Conversation

@futjesus

Copy link
Copy Markdown
Member

Summary

Root-cause fix for tables getting stranded on the loading skeleton (reported from compute instances list, civo/dashboard/compute-micro-frontend!187 / issue #75), plus the follow-up features that remove the need for consumer-side workarounds.

🐛 Query lifecycle fix

  • initialData seeded [] on every query key, so isLoading was always false and the skeleton gated on isFetching (flashing on every background poll). Consumers suppressed the flicker with notifyOnChangeProps: ['data','error'], which dropped the isFetching:false re-render whenever a refetch resolved to structurally-equal data → skeleton stranded forever.
  • Now: skeleton on initial load and search/filter/page changes, silent background refetches, and consumer notifyOnChangeProps is merged with the props the provider reads so the table can never strand again. Side effects moved out of queryFn into a dataUpdatedAt-keyed effect. Query-key building no longer mutates the consumer id array.

✨ New API

  • errorState?: ReactNode | ((error: Error) => ReactNode) — shown when the query failed and there are no rows; failures during background polls keep the current rows.
  • headerContent + classNameHeaderContent — full-width band inside the <thead> above the column headers (Figma 2926-11781); top rounded corners move to the band, including the expand-column handoff.
  • classNameScrollContainer — opt-in horizontal scroll region wrapping table + pagination while the filter row stays fixed.
  • dropdownPaginationDirection?: 'auto' | 'up' | 'down' — page-size dropdown flips up only when the viewport has no room below; can be fixed.
  • Stable kvt-* class hooks on every table section for external styling.

♻️ Internals

  • The 5 Filter dropdowns migrate to a shared Radix Popover FilterDropdown (portal, collision handling, Escape-to-close) so overflow containers never clip them. New dependency: @radix-ui/react-popover.
  • Page-size dropdown rebuilt on Radix Popover reusing the Select variants; the 150px reserved spacer below the pagination bar is gone.
  • Valueless useCallback/useMemo removed across the component.
  • vite-plugin-dts entryRoot pinned so dist/ keeps the lib/ layout copy.sh expects (broke after the ⬆️ chore: update dependencies to latest compatible versions #665 dependency bump).

Consumer migration (next alpha release)

  • Delete notifyOnChangeProps: ['data','error'], placeholderData: keepPreviousData and refetchOnMount: true workarounds from queryOptions.
  • Tables preferring to keep old rows during a search can opt back in with queryOptions={{ placeholderData: keepPreviousData }}.
  • Move overflow-x-auto contain-inline-size from classNameWrapperTable to classNameScrollContainer to include pagination in the scroll region.
  • Follow-up MRs are open in the affected micro-frontends (compute, kubernetes, settings, vpc) and must merge after this releases.

Test plan

  • 490 tests pass (37 files), including new integration coverage: stuck-skeleton regression with suppressed notifications, errorState precedence, keepPreviousData opt-in, headerContent corner handoff, scroll container structure, portaled dropdown click-inside/outside, page-size direction.
  • npm run check:types, npm run lint, npm run build clean; verified manually via Storybook and against compute-micro-frontend.

futjesus added 9 commits July 21, 2026 20:44
…strands

The provider seeded initialData on every query key, forcing isLoading to
stay false and the skeleton to gate on isFetching. Consumers suppressed
the resulting poll flicker with notifyOnChangeProps: ['data','error'],
which silently dropped the isFetching:false re-render whenever a refetch
resolved to structurally-equal data, stranding the table on the skeleton.

- Seed initialData only for the first cache entry and only when rows were
  provided, so isLoading regains meaning (skeleton on initial load and
  key changes, silent background refetches)
- Merge consumer notifyOnChangeProps with the props the provider reads so
  internal re-renders can never be suppressed
- Move queryFn side effects into an effect keyed on dataUpdatedAt, which
  changes on every successful fetch regardless of structural sharing
- Gate Skeleton/Pagination/WrapperBody on tableLoading instead of
  tableFetching, expose tableError and headerContent through the context
- Stop mutating the consumer id array when building the query key
- Move root tests into tests/ and add fetch integration coverage
…izedTable

- errorState?: ReactNode | ((error: Error) => ReactNode) renders in place
  of rows when the query failed and there is nothing to show; background
  refetch failures keep the current rows. With React Query's default
  retry it appears after retries; queryOptions={{ retry: false }} shows
  it immediately
- headerContent renders a full-width band inside the thead above the
  column headers (scrolls with the table, visible in every state) and
  takes over the top rounded corners, including the expand-column handoff
- classNameScrollContainer opts into a horizontal scroll region wrapping
  the table body and pagination bar while the filter row stays fixed to
  the container width
- Stable kvt-* class hooks on every section (filter, search, header,
  band, body, rows, cells, states, pagination) so consumers can restyle
  any part from outside
- Stories for HeaderContent, ErrorState and HorizontalScrollWithFilters
Filter dropdown menus were hand-rolled absolute divs that any overflow
container clipped (overflow-x forces overflow-y), blocking the new table
scroll region. All five dropdowns now share a Radix Popover based
FilterDropdown (portaled, collision-aware, Escape to close); the manual
mousedown outside-click handlers are gone because they would close the
portaled menus from inside.

The pagination page-size dropdown is rebuilt on Radix Popover reusing
the Select variants: direction 'auto' flips up only when the viewport
has no room below (dropdownPaginationDirection fixes it to 'up'/'down'),
and it portals automatically when classNameScrollContainer is set. The
150px spacer that reserved room below the bar is no longer needed.
Removes memoization whose result feeds inline arrows or plain DOM
elements, plus trivial boolean/number computations. Callbacks exposed
through the table context, column-identity memos and the search debounce
stay memoized because consumers depend on their identity.
npm 11 prunes transitive deps of wasm platform packages when resolving
next to an existing node_modules, which left the lock missing @emnapi
entries npm ci requires. Regenerated from origin/main's lock in a clean
directory; validated with npm ci --dry-run on npm 10 and 11.
@futjesus
futjesus merged commit c574c28 into main Jul 21, 2026
1 check passed
@futjesus
futjesus deleted the feat/virtualized-table-overhaul branch July 21, 2026 20:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant