Skip to content

DataGrid ​

A feature-rich, headless table component built on TanStack Table with support for sorting, filtering, pagination, row selection, real-time WebSocket updates, and master-detail expansion.

Installation ​

bash
npx shadcn@latest add https://befame-registry.example.com/r/data-grid.json

Basic Usage ​

tsx
import { DataGrid } from '@/components/data-grid/data-grid'
import { createColumnHelper } from '@tanstack/react-table'
import type { ColumnDef } from '@/components/data-grid/types'

interface User {
  id: number
  name: string
  email: string
}

const col = createColumnHelper<User>()

const columns: ColumnDef<User>[] = [
  col.accessor('id', { header: 'ID' }),
  col.accessor('name', { header: 'Name' }),
  col.accessor('email', { header: 'Email' }),
]

const users: User[] = [
  { id: 1, name: 'Alice', email: 'alice@example.com' },
  { id: 2, name: 'Bob', email: 'bob@example.com' },
]

export function BasicExample() {
  return <DataGrid columns={columns} data={users} />
}

Feature Flags ​

All features are opt-in via the features prop:

tsx
<DataGrid
  columns={columns}
  data={users}
  features={{
    sorting: true,
    filtering: true,
    pagination: true,
    rowSelection: true,
    expanding: true,
  }}
/>

Column Headers with Sorting ​

Use DataGridColumnHeader to get a sortable column header with the standard sort indicator:

tsx
import { DataGridColumnHeader } from '@/components/data-grid/data-grid-column-header'

const columns: ColumnDef<User>[] = [
  col.accessor('name', {
    header: ({ column }) => <DataGridColumnHeader title="Name" column={column} />,
    meta: { label: 'Full Name' }, // used in the column-visibility menu
  }),
  col.accessor('email', {
    header: ({ column }) => <DataGridColumnHeader title="Email" column={column} />,
  }),
]

Server-Side Pagination with a Store ​

createDataGridStore wires up a paginated server endpoint. The grid handles paging, sorting and filtering automatically.

tsx
import { createDataGridStore } from '@/components/data-grid/lib/create-store'

// REST endpoint — the grid appends ?skip=0&take=10&sort=...&filter=...
const usersStore = createDataGridStore<User>({
  key: ['users'],
  endpoint: '/api/users',
  searchFields: ['name', 'email'], // fields searched when the user types in the toolbar
})

export function ServerSideExample() {
  return (
    <DataGrid
      columns={columns}
      store={usersStore}
      features={{ sorting: true, filtering: true, pagination: true }}
      pagination={{ pageSize: 10, pageSizeOptions: [5, 10, 25, 50] }}
    />
  )
}

Custom Loader ​

Use a custom load function instead of an endpoint for non-REST sources, auth headers, GraphQL, etc.

tsx
const usersStore = createDataGridStore<User>({
  key: ['users'],
  load: async (req) => {
    const res = await fetch('/api/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(req),
    })
    return res.json() // must return { data: TData[], total: number }
  },
})

Invalidating / Refetching from Outside the Grid ​

The store exposes invalidate and refetch so you can trigger a reload after mutations:

tsx
import { useQueryClient } from '@tanstack/react-query'

function DeleteUserButton({ userId }: { userId: number }) {
  const queryClient = useQueryClient()

  const handleDelete = async () => {
    await fetch(`/api/users/${userId}`, { method: 'DELETE' })
    await usersStore.invalidate(queryClient) // grid reloads automatically
  }

  return <Button onClick={handleDelete}>Delete</Button>
}

TanStack Query (queryResult) ​

If you already use useQuery in your component, pass the result directly:

tsx
import { useQuery } from '@tanstack/react-query'

function UsersPage() {
  const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 })

  const query = useQuery({
    queryKey: ['users', pagination],
    queryFn: () =>
      fetch(`/api/users?skip=${pagination.pageIndex * pagination.pageSize}&take=${pagination.pageSize}`)
        .then(r => r.json()),
  })

  return (
    <DataGrid
      columns={columns}
      queryResult={query}
      features={{ pagination: true }}
      pagination={{
        manual: true,
        rowCount: query.data?.total,
        pageSize: 10,
        onPaginationChange: setPagination,
      }}
    />
  )
}

Row Selection ​

tsx
<DataGrid
  columns={columns}
  data={users}
  features={{ rowSelection: true }}
  onSelectionChange={(rows) => console.log('Selected rows:', rows)}
  onRowSelectedChange={(row, isSelected) => console.log(row.name, isSelected)}
  // Pre-select rows on load
  getRowInitiallySelected={(row) => row.status === 'active'}
/>

Row Actions ​

Add a dedicated actions column with custom buttons per row:

tsx
<DataGrid
  columns={columns}
  data={users}
  rowActions={{
    size: 120, // column width in px
    render: (user) => (
      <div className="flex gap-2">
        <Button size="sm" variant="outline" onClick={() => viewUser(user)}>View</Button>
        <Button size="sm" variant="outline" onClick={() => editUser(user)}>Edit</Button>
      </div>
    ),
  }}
/>

Row Styling with onRowPreparing ​

Customize per-row <tr> props (class, style, data attributes, event handlers) based on row data:

tsx
<DataGrid
  columns={columns}
  data={users}
  onRowPreparing={({ data, isSelected, rowIndex }) => {
    if (data.status === 'inactive') {
      return { className: 'opacity-60 italic' }
    }
    if (isSelected) {
      return { className: 'bg-blue-50' }
    }
    return rowIndex % 2 === 1 ? { className: 'bg-muted/30' } : undefined
  }}
/>

Master-Detail (Row Expansion) ​

Render an expandable detail panel below each row:

tsx
<DataGrid
  columns={columns}
  data={users}
  features={{ expanding: true }}
  renderSubComponent={(user) => <UserDetailPanel user={user} />}
  // Optionally restrict which rows can expand
  getRowCanExpand={(user) => user.role === 'admin'}
  onExpandedChange={(expandedRows) => console.log('Expanded:', expandedRows)}
/>

Custom Toolbar Content ​

Inject buttons or other controls into the toolbar alongside the built-in search:

tsx
<DataGrid
  columns={columns}
  data={users}
  features={{ filtering: true }}
  toolbar={{
    columnVisibility: true, // show/hide column toggle
    children: (
      <Button variant="outline" onClick={handleExport}>
        Export CSV
      </Button>
    ),
  }}
/>

Loading State ​

tsx
// Default skeleton rows
<DataGrid columns={columns} data={[]} loadingConfig={{ isLoading: true, rows: 8 }} />

// Custom loading component
<DataGrid
  columns={columns}
  data={[]}
  loadingConfig={{
    isLoading: true,
    component: <MySpinner />,
  }}
/>

Zod Validation ​

Drop invalid rows before rendering and surface errors to the consumer:

tsx
import { z } from 'zod'

const userSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
})

<DataGrid
  columns={columns}
  data={untrustedData}
  schema={userSchema}
  onValidationError={(row, error) => {
    console.warn('Invalid row dropped:', row, error.issues)
  }}
/>

Real-Time WebSocket Updates ​

tsx
<DataGrid
  columns={columns}
  data={initialData}
  websocket={{
    url: 'wss://your-server.com/live/users',
    onMessage: (event, currentData) => {
      const update = JSON.parse(event.data)
      return currentData.map(row => (row.id === update.id ? update : row))
    },
    reconnectInterval: 2000,
    maxReconnectAttempts: 10,
  }}
/>

API Reference ​

DataGrid Props ​

PropTypeDefaultDescription
dataTData[]—Static data array. Use this or queryResult/store.
queryResultUseQueryResult<DataGridResponse<TData>>—TanStack Query result. The grid reads data, isFetching.
storeDataGridStore<TData>—Self-managed paginated store. Created via createDataGridStore. Enables server-side pagination automatically.
columnsColumnDef<TData>[]requiredTanStack Table column definitions.
featuresDataGridFeatures{}Feature flags (sorting, filtering, pagination, rowSelection, expanding).
paginationDataGridPaginationConfig{}Pagination settings.
toolbarDataGridToolbarProps—Toolbar config; pass children to add custom controls.
schemaz.ZodType<TData>—Zod schema for row validation. Invalid rows are dropped.
onValidationError(row, error: ZodError) => void—Called when a row fails schema validation.
websocketDataGridWebSocketConfig—Real-time update config.
onRowClick(row: Row<TData>) => void—Called when a row is clicked. Adds cursor-pointer to rows.
onSelectionChange(rows: TData[]) => void—Called when the selected row set changes.
onRowSelectedChange(row: TData, isSelected: boolean) => void—Called when a single row's selection state toggles.
getRowInitiallySelected(data: TData) => boolean—Seed the initial selection state from row data on load.
onRowPreparing(info: RowPreparingInfo<TData>) => RowPreparingResult | void—Customize <tr> props per row (className, style, event handlers).
renderSubComponent(row: TData) => ReactNode—Render the expanded detail panel (requires features.expanding).
getRowCanExpand(row: TData) => boolean() => trueControl which rows show the expand toggle.
onExpandedChange(expandedRows: TData[]) => void—Called when rows are expanded or collapsed.
rowActionsRowActionsConfig<TData>—Add an actions column with custom buttons.
loadingConfigDataGridLoadingConfig{}Loading state display (skeleton rows or custom component).
emptyMessagestring'No results.'Message displayed when there are no rows.
classNamestring—Extra class applied to the outer wrapper.

DataGridFeatures ​

PropTypeDefaultDescription
sortingbooleantrueEnable column sorting.
filteringbooleantrueEnable global text search in the toolbar.
paginationbooleantrueEnable pagination controls.
rowSelectionbooleanfalseEnable row selection checkboxes.
expandingbooleanfalseEnable master-detail row expansion (requires renderSubComponent).

DataGridPaginationConfig ​

PropTypeDefaultDescription
pageSizenumber10Initial number of rows per page.
pageSizeOptionsnumber[]—Available page-size choices shown in the pagination footer.
manualbooleanfalseEnable server-side pagination. The grid won't slice data itself.
rowCountnumber—Total row count from the server. Required in manual mode.
onPaginationChange(pagination: PaginationState) => void—Called when the page or page size changes (use to refetch the server page).

DataGridStoreConfig ​

PropTypeDefaultDescription
keyQueryKeyrequiredBase TanStack Query key. Invalidating this key refreshes all pages.
endpointstring—REST endpoint. The grid appends ?skip=&take=&sort=&filter=. Provide this or load.
load(req: DataGridRequest) => Promise<DataGridResponse<TData>>—Custom async loader (for auth, GraphQL, non-REST, etc.).
paramsRecord<string, string | number | boolean>—Extra static query params appended to every request (endpoint mode).
fetchertypeof fetchfetchCustom fetch implementation (e.g. to add auth headers).
searchFieldsstring[]—Field names used to build a contains filter from the toolbar search input.

DataGridWebSocketConfig ​

PropTypeDefaultDescription
urlstringrequiredWebSocket URL (ws://, wss://, or PartyKit project URL).
onMessage(event: MessageEvent, currentData: TData[]) => TData[]—Transform an incoming message into updated row data.
reconnectIntervalnumber1000Reconnect interval in ms.
maxReconnectAttemptsnumber5Max reconnect attempts before giving up.

RowActionsConfig ​

PropTypeDefaultDescription
render(row: TData) => ReactNode | ReactNode[]requiredRender action buttons for a row.
sizenumber100Width of the actions column in pixels.

DataGridLoadingConfig ​

PropTypeDefaultDescription
isLoadingboolean—Whether the grid is in a loading state.
rowsnumber5Number of skeleton rows to show while loading.
componentReactNode—Custom component rendered instead of the default skeleton.

RowPreparingInfo ​

Passed to onRowPreparing for each rendered row.

FieldTypeDescription
rowRow<TData>TanStack Table row object.
dataTDataThe row's underlying data object.
rowIndexnumberZero-based index within the current page.
isSelectedbooleanWhether the row is currently selected.
isExpandedbooleanWhether the row is currently expanded.

DataGridRequest ​

Shape of the paging request sent to your server (DevExtreme LoadOptions pattern).

FieldTypeDescription
skipnumberRows to skip (zero-based offset).
takenumberRows to return (page size).
filterstring | nullDevExtreme filter JSON, e.g. '["name","contains","john"]'.
sortstring | nullDevExtreme sort JSON, e.g. '[["name","asc"]]'.

DataGridResponse ​

Shape of the paginated response your server must return.

FieldTypeDescription
dataTData[]Rows for the current page.
totalnumberTotal row count across all pages.

DataGridColumnMeta ​

Optional metadata you can attach to any column definition via meta:

FieldTypeDescription
labelstringHuman-readable display name used in the column-visibility menu. Falls back to the string header, then a humanized column id.
hideablebooleanWhether this column can be hidden via the column-visibility toggle.
filterablebooleanWhether this column participates in column-level filtering.