Logo VitNode
Documentation

Data Table

Data Table

A typed table with sorting, search, filters and pagination that keeps its state in the URL and reads its rows from the VitNode API.

Preview

Usage

import type { ColumnDef } from '@vitnode/core/components/table/data-table-content'

import { ContentDataTable } from '@vitnode/core/components/table/content'
const columns: ColumnDef<User>[] = [
  { accessorKey: 'name', header: 'Name' },
  { accessorKey: 'email', header: 'Email' },
  { accessorKey: 'status', header: 'Status', align: 'center' },
  {
    id: 'actions',
    header: '',
    align: 'right',
    cell: ({ row }) => <EditUserButton id={row.id} />,
  },
]

<ContentDataTable
  id="users-table"
  columns={columns}
  edges={data.edges}
  pageInfo={data.pageInfo}
  order={{ defaultOrder: { column: 'name', order: 'asc' } }}
/>

Every row needs a numeric id. Render the table inside a DataTableNavigationProvider, or its controls throw.

Connect it to the URL

The table never talks to the router. Search, filters, sorting and the pager all hand a new query string to navigate, and the table renders whatever searchParams says.

import type { DataTableNavigation } from '@vitnode/core/components/table/navigation'

import { DataTableNavigationProvider } from '@vitnode/core/components/table/navigation'

const navigation = React.useMemo<DataTableNavigation>(
  () => ({
    searchParams: new URLSearchParams(search),
    navigate: async nextSearch => {
      await navigate({
        resetScroll: false,
        search: Object.fromEntries(new URLSearchParams(nextSearch)),
      })
    },
  }),
  [navigate, search],
)

<DataTableNavigationProvider value={navigation}>
  <ContentDataTable {...props} />
</DataTableNavigationProvider>

The route's loader then fetches the page those params describe:

const res = await fetcher({
  plugin: '@vitnode/core',
  method: 'get',
  module: 'admin/users',
  path: '/list',
  args: { query: search },
  withPagination: true,
})

Columns

An accessor column binds to a field with accessorKey, which only accepts keyof T, so typos fail at compile time. A display column (actions, badges) has no field and needs a string id.

cell replaces the default text and gets { row, allData }. align sets 'left' (default), 'center' or 'right' for the header and cells. Right suits numbers and actions.

Sorting

List sortable columns in order.columns. Clicking a header flips between ascending and descending and writes ?orderBy=&order= to the URL. defaultOrder applies when the URL says nothing.

order={{
  columns: ['name', 'createdAt'],
  defaultOrder: { column: 'createdAt', order: 'desc' },
}}

Clickable rows

rowOpens makes the whole row a target. It's focusable, and Enter or Space opens it too. Clicks on links, buttons and inputs inside the row are ignored.

rowOpens={user => navigate({ to: `/admin/core/users/${user.id}` })}

Features

API

Prop

Type

Other props go to the underlying Table.