Skip to content

feat: add composable DataTable component#18

Merged
yahyafakhroji merged 13 commits intomainfrom
feat/data-table
Mar 13, 2026
Merged

feat: add composable DataTable component#18
yahyafakhroji merged 13 commits intomainfrom
feat/data-table

Conversation

@yahyafakhroji
Copy link
Copy Markdown
Contributor

@yahyafakhroji yahyafakhroji commented Mar 9, 2026

Summary

  • New DataTable compound component with a reactive store architecture (useSyncExternalStore) featuring selector-based hooks for minimal re-renders
  • Client-side mode: built-in filtering (checkbox, select, date-range), sorting (tri-state), pagination, and search with useTransition for non-blocking UI
  • Server-side mode: cursor-based pagination, fetch lifecycle management, and fetchFn/transform pattern
  • Pre-filtering engine with self-registering filter components (register strategy on mount) and built-in strategies (checkbox, select, date-gte, date-lte)
  • ActiveFilters component with grouped filter badges, customizable labels, clear-all variants, and data-slot support
  • nuqs adapter for URL state sync with simple sort format (-column for desc)
  • Selection column auto-injection with bulk actions support
  • SSR-safe providers using inline props API — no more hook-and-spread pattern
  • Accessibility: aria-labels on pagination/search/column headers, aria-current="page", nav landmarks
  • 320+ tests across 20 test files covering store, hooks, filters, providers, and all components
  • Storybook stories and Fumadocs documentation for the full API

Sidebar Refactor

  • Split sidebar.tsx into base/sidebar/ (layout primitives, no motion dependency) and features/app-navigation/ (renamed from AppSidebarAppNavigation, NavMainNavMenu)
  • Updated storybook and docs accordingly

Dependency & DX Changes

  • Bundle Radix UI and cmdk as regular dependencies (no longer peer deps) — simplified consumer install
  • Make lucide-react a required peer dep (used by base sidebar primitives)
  • Add tw-animate-css to root styles for component animations
  • Add "source" export condition to all package exports for instant Storybook HMR (resolves TS source directly, bypasses tsdown)
  • Smart predev guard — only builds datum-ui when dist/ is missing, preventing turbo race conditions
  • Alpha publish workflow in CI for PR preview releases

@yahyafakhroji yahyafakhroji marked this pull request as ready for review March 9, 2026 11:16
@yahyafakhroji yahyafakhroji added release:minor Bump minor version on publish alpha Publish alpha version to npm labels Mar 10, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 10, 2026

📦 Alpha published

@datum-cloud/datum-ui@0.3.0-alpha.4702cdb

pnpm add @datum-cloud/datum-ui@0.3.0-alpha.4702cdb
# or use the alpha tag for the latest alpha:
pnpm add @datum-cloud/datum-ui@alpha

@yahyafakhroji
Copy link
Copy Markdown
Contributor Author

@gaghan430 The data-table component can now be reviewed and tested. You can use the alpha version

@gaghan430
Copy link
Copy Markdown

@gaghan430 The data-table component can now be reviewed and tested. You can use the alpha version

I'll try it out... thanks!

@gaghan430
Copy link
Copy Markdown

gaghan430 commented Mar 10, 2026

Monosnap.screencast.2026-03-10.20-25-49.mp4

@yahyafakhroji I have a few things for now and will continue testing further:

  • The table feels a bit laggy compared to the existing staff portal table, especially when sorting.
  • There doesn’t seem to be a way to return to the unsorted state. In the existing table, the flow is: sort asc → sort desc → remove sort.
  • The pagination doesn’t seem to be working properly. Also, since this is client-side processing and we already have all the data, would it be possible to use a detailed pagination style instead of only previous/next (cursor-based) pagination?
  • Would it also be possible to have an active filter component similar to the one in the staff portal?
  • I also noticed that the style doesn’t seem to follow the Alpha theme. For example, the search field is using a different style.

I’ll continue testing and will share more feedback if I find anything else.

…cture

- Reactive store (useSyncExternalStore) with selector hooks for re-render isolation
- Pre-filtering engine with built-in strategies (checkbox, select, date-gte, date-lte)
- Self-registering filter components (register strategy on mount)
- Client-side mode: filtering, sorting, pagination, search with useTransition
- Server-side mode: cursor-based pagination, fetch lifecycle management
- nuqs adapter for URL state sync with simple sort format (-column for desc)
- Selection column auto-injection with bulk actions
- Inline content registration system
- React.memo on all leaf components for performance
- 320 tests across 48 test files
- Storybook stories and docs page
- CI: alpha publish workflow for PR previews
…se components

- Add DataTable.ActiveFilters compound component with grouped filter
  badges, customizable label, clear-all variants (icon/button/text),
  and className/data-slot support for styling
- Migrate all data-table components from @repo/shadcn imports to
  datum-ui base components (Button, Input, Badge, Select, etc.) for
  Alpha theme consistency
- Fix Badge variant prop in checkbox-filter (variant -> type/theme)
- Update storybook stories with ActiveFilters demos and customization
- Update fumadocs docs with ActiveFilters section, tri-state sorting,
  server fetchFn/transform pattern, and new data-slot entries
…race condition

- Add aria-labels to pagination prev/next and page number buttons
- Add aria-current="page" to active page button
- Wrap pagination buttons in nav landmark
- Add aria-label to search input
- Add descriptive aria-label with sort state to column header button
- Add setPagination batch method to avoid double state updates
- Add input validation on setPageIndex/setPageSize (NaN/negative guard)
- Use immutable Map reassignment in registerFilter/unregisterFilter
- Merge cursor-reset effect into fetch effect to fix server hook race condition
- Replace separate useState(hasNextPage) with ref to avoid extra render cycle
@yahyafakhroji
Copy link
Copy Markdown
Contributor Author

yahyafakhroji commented Mar 11, 2026

Addressing review feedback

Reference: #18 (comment)

Here's a summary of what was done to address each point from the review:


1. "The table feels a bit laggy compared to the existing staff portal table, especially when sorting"

  • Selector hooks (useDataTableFilters, useDataTableSearch, useDataTablePagination, etc.) now isolate re-renders — only components that subscribe to the changed slice re-render
  • memo() applied to performance-sensitive components (ActiveFilters, Content)
  • Store uses immutable state updates with minimal object creation

2. "There doesn't seem to be a way to return to the unsorted state (asc → desc → remove sort)"

  • Tri-state sorting cycle is now enabled: unsorted → asc → desc → unsorted
  • Documented in fumadocs under the Sorting section

3. "Pagination doesn't seem to be working properly / use detailed pagination instead of cursor-based"

  • Client-side pagination now shows numbered page buttons (1, 2, 3...) with ellipsis for large page counts, plus previous/next buttons
  • Shows "Showing X to Y of Z rows" info and rows-per-page selector
  • setPagination batch method added to avoid double state updates and stale closures
  • Input validation on setPageIndex/setPageSize (NaN/negative guard)
  • Server hook race condition fixed by merging cursor-reset effect into fetch effect
  • A11y: pagination wrapped in <nav> landmark with aria-label, aria-current="page" on active page

4. "Would it be possible to have an active filter component similar to the staff portal?"

  • DataTable.ActiveFilters component added with the same grouped box pattern as the staff portal
  • Uses datum-ui Badge and Button components for Alpha theme consistency
  • Features:
    • Filters grouped by column in bordered boxes
    • Individual value removal (including single values from multi-select arrays)
    • Search term shown as a separate badge
    • Clear all button with 3 variants: icon (default), button, text
    • Customizable label, filterLabels for human-readable column names
    • className, groupClassName, badgeClassName props + data-slot attributes
  • 22 tests covering all variants and edge cases

5. "The style doesn't seem to follow the Alpha theme (e.g., search field)"

  • Migrated all components to use datum-ui base components (Badge, Button, Input, Select, etc.)
  • Fixed prop mapping: varianttype/theme for datum-ui CVA variant system
  • Search input now uses datum-ui Input component with proper aria-label
  • Column header sort button has descriptive aria-label with sort state

Full changelog

Commit 1: feat(data-table): add DataTable component with reactive store architecture

  • Reactive store with useSyncExternalStore + fine-grained selector hooks
  • Compound component API: DataTable.Client, .Server, .Content, .Pagination, .Search, .ColumnHeader, .RowActions, .BulkActions, .InlineContent, .Loading
  • Pre-filtering engine with self-registering strategies (exact, includes, dateAfter, dateBefore)
  • Filter components: SelectFilter, CheckboxFilter, DatePickerFilter
  • Client hook with full sorting, filtering, pagination, row selection
  • Server hook with cursor-based pagination (fetchFn/transform pattern)
  • URL sync adapter via nuqs
  • Selection column with select-all header
  • Inline content (top-of-table and per-row forms)
  • className props + data-slot attributes on every component
  • 22 test files, 324 tests
  • 7 Storybook stories + full fumadocs page

Commit 2: feat(data-table): add ActiveFilters component, migrate to datum-ui base components

  • ActiveFilters component with grouped filter box pattern (staff portal style)
  • Badge/Button migration to datum-ui base components
  • 22 new tests for ActiveFilters
  • Updated Storybook stories + docs

Commit 3: fix(data-table): a11y improvements, store hardening, and server hook race condition

  • 5 a11y fixes (nav landmark, aria-labels, aria-current on pagination, search, column header)
  • 3 store hardening fixes (setPagination batch, input validation, immutable Map)
  • 1 server hook race condition fix (merged effects, ref instead of state)

mattdjenkinson
mattdjenkinson previously approved these changes Mar 11, 2026
@gaghan430
Copy link
Copy Markdown

@yahyafakhroji Im getting this error using latest package, could be related with data-table
image
image
do I need to do something specific to use data-table now?

@cla-assistant
Copy link
Copy Markdown

cla-assistant bot commented Mar 11, 2026

CLA assistant check
All committers have signed the CLA.

Add getServerSnapshot to all useSyncExternalStore calls for React 18+
SSR compatibility. Refactor providers to accept hook options directly
as props, creating store/table internally with a two-layer SSR gate
(useIsClient → conditional render).

Breaking changes:
- useDataTableClient and useDataTableServer removed from public exports
- useDataTableContext deprecated facade removed
- Providers no longer accept store/table props; pass data/columns/etc. directly

Changes:
- Add useIsClient hook for SSR/hydration detection
- Rewrite ClientProvider and ServerProvider with inner/outer pattern
- Add ssrFallback prop for custom loading state during SSR
- Remove useDataTableContext deprecated facade from use-selectors
- Delete hooks/use-data-table-context.ts re-export file
- Update all tests and storybook stories to inline API
…race condition

- Replace use() with useContext() for DataTableRenderKeyContext to fix
  SSR context subscription bug where use() fails to register subscriptions
  after React Router SSR hydration, causing sort/pagination/page-size
  changes to not re-render components
- Add _version monotonic counter to store state, incremented on every
  setState, powering DataTableRenderKeyContext for table-dependent hooks
- Wrap providers (client + server) with DataTableRenderKeyContext
- Rewrite useDataTableSelection, useDataTablePagination, useDataTableRows
  to use useRenderKey() + direct table reads instead of useSliceSelector
- Fix pagination active button using disabled styling instead of full
  opacity (aria-disabled + pointer-events-none instead of disabled prop)
@yahyafakhroji
Copy link
Copy Markdown
Contributor Author

yahyafakhroji commented Mar 11, 2026

@gaghan430 Just pushed a fix — can you try again with the latest from this branch - @datum-cloud/datum-ui (0.3.0-alpha.bdce331) ?

If you're still seeing issues, could you share which version you're on and how you're importing the data-table?

- Add "source" condition to all datum-ui package exports pointing to
  TypeScript source files (CSS-only exports ./grid and ./nprogress
  intentionally omitted to avoid PostCSS resolving TS files as CSS)
- Configure storybook rsbuild to prefer "source" conditionNames so
  @datum-cloud/datum-ui resolves to src/ directly, bypassing tsdown
- Replace unconditional predev in storybook and docs with a guard that
  only builds datum-ui when dist is missing, preventing race conditions
  when running via turbo while still supporting direct pnpm dev runs
- Update docs to use inline props pattern (no more useDataTableClient/
  useDataTableServer hook-and-spread) matching the actual SSR-safe API
- Deduplicate types: hooks now import from types.ts instead of defining
  local copies that diverged (ServerTransformResult.nextCursor vs cursor)
- Fix runtime to read result.cursor matching the public type
- Add missing filterFns, className, ssrFallback props to docs and types
…perf

- Add disabled prop to Search, SelectFilter, CheckboxFilter, DatePickerFilter
- Add native disabled prop to CalendarDatePicker (replaces CSS workaround)
- Extract FilterOption type and DataTableBaseProps to reduce duplication
- Extract shared test-helpers.tsx (mockTable, createTestStore, renderWithStore)
- Convert excludeFilters to Set for O(1) lookups in ActiveFilters
- Add dot-path resolution for nested filter column access
- SSR fallback defaults to skeleton table instead of blank
- Rewrite Loading skeleton to use shadcn Table components
- Add excludeFilters support with reserved 'search' key
- Update docs with disabled state, filterFns, and dot-notation sections
SelectFilter and CheckboxFilter triggers defaulted to h-9 via datum-ui
Button, causing a 4px mismatch with the Search input (h-10). Added
explicit h-10 to both filter trigger buttons for consistent height
within DataTable context. Consumer className overrides still work.
Radix UI and cmdk packages are now regular dependencies instead of
peerDependencies, so consumers no longer need to install them manually.
Added tw-animate-css to root styles for component animations. Updated
all 27 component docs to remove radix/cmdk from install instructions.
Move sidebar.tsx to base/sidebar/ so consumers can use the base
sidebar layout without installing motion. Rename AppSidebar to
AppNavigation and NavMain to NavMenu in new app-navigation export.
Update storybook stories and docs accordingly.
- Make lucide-react a required (non-optional) peer dependency since
  base sidebar primitives import it directly
- Export sidebar from base barrel (base/index.ts)
- Add h-10 height to DatePickerFilter trigger for consistent filter alignment
- Pass className through to DataTableLoading SSR fallback
- Extract skeleton to dedicated file with datum-ui customization
…Value API

Remove the SSR hydration gate (useIsClient) and make the store always
available during SSR. Table instance is deferred to after hydration via
a tableReady state gate, with null-safe selector hooks throughout.

- Remove useIsClient hook and SSR fallback rendering
- Add useTableInstanceOrNull for null-safe table access during SSR
- Refactor ClientProvider into outer (store) + inner (table) components
- Extract useClientTable and useServerTable hooks from providers
- Add loading prop to DataTable.Client for consumer-controlled skeleton state
- Add columnCount to store so Content derives skeleton columns from columns.length
- Fix loading effect ordering to prevent empty-message flash
- Refactor formatFilterValue from function to Record<string, (value) => string>
  with dot-notation support
- Remove dead setTable from store interface
- Deprecate useTableInstance with accurate hydration error message
- Update docs, storybook, and tests for all changes
@yahyafakhroji
Copy link
Copy Markdown
Contributor Author

yahyafakhroji commented Mar 13, 2026

Today's Changes (Mar 13)

SSR Fallback Removal, Loading Prop & formatFilterValue API (4702cdb)

SSR Architecture Rewrite:

  • Removed useIsClient hook and the SSR fallback rendering gate entirely
  • Store is now always available during SSR — no more dummy store or hydration mismatch
  • Table instance deferred to after hydration via tableReady state gate
  • Added useTableInstanceOrNull for null-safe table access; all selector hooks migrated to use it
  • Refactored ClientProvider into outer (store creation) + inner (table creation) components
  • Extracted useClientTable and useServerTable hooks from provider components

New loading Prop on DataTable.Client:

  • Consumer-controlled loading state: <DataTable.Client loading={isLoading}>
  • DataTable.Content automatically shows skeleton rows (matching pageSize x column count) when loading
  • No more manual {isLoading ? <DataTable.Loading /> : <DataTable.Content />} ternary needed
  • Loading effect placed in parent provider (gated by tableReady) to prevent empty-message flash from React effect ordering

formatFilterValue Record API:

  • Changed from (column: string, value: unknown) => string | undefined to Record<string, (value) => string>
  • Keys support dot notation (e.g. 'status.approval')
  • Consumer-typed values — annotate (value: string) at call site for type safety

Docs & Storybook:

  • Removed ssrFallback from Client/Server props tables
  • Added loading prop documentation
  • Updated all examples to use new loading prop pattern instead of DataTable.Loading ternary
  • Updated formatFilterValue examples and props table for record API
  • Updated DataTable.Content and DataTable.Loading descriptions

Copy link
Copy Markdown

@gaghan430 gaghan430 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@yahyafakhroji yahyafakhroji merged commit 06143f8 into main Mar 13, 2026
7 checks passed
@yahyafakhroji yahyafakhroji deleted the feat/data-table branch March 13, 2026 15:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

alpha Publish alpha version to npm release:minor Bump minor version on publish

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Build Generic, Composable DataTable Component for @datum-cloud/datum-ui

3 participants