Displays tabular data with features such as sorting and pagination

DataTable provides complex features for tables, like sorting and pagination. It's built around the @tanstack/react-table library and exposes the table state from that library directly. All util functions from the library are also compatible with DataTable. It's worth a good read of @tanstack/react-table's documentation too, since we won't be repeating much of it here.

DataTable and its subcomponents are designed to be very simple to use. This is achieved by abstracting complex and/or boilerplate logic away from the consumer. For example, DataTable.Pagination can be dropped inside a DataTable to paginate the table's data without the need to add any configuration on DataTable itself. This is achieved by having DataTable.Pagination itself use the applyPagination and setPageSize methods exposed by useDataTable on its first render. This pattern should be replicated wherever practical to maintain the best developer experience possible.

Anatomy

The root DataTable component manages the table's state and exposes it via the React Context API. This state can be accessed by any child components by calling useDataTable. You can pass an initialState prop to DataTable as described in the @tanstack/react-table docs.

Other DataTable components call useDataTable and provide useful default implementations for common patterns. For example, DataTable.Head will render a header for every column defined in the parent DataTable. DataTable.Body will render a row for every data item. DataTable.Table combines both DataTable.Head and DataTable.Body.

Using defaults vs rolling your own

Here's a simple config for some table data and columns.

// import { createColumnHelper } from '@tanstack/react-table'
const columnHelper = createColumnHelper<{
name: string
hobby: string
}>()
const columns = [
columnHelper.accessor('name', {
cell: (info) => info.getValue()
}),
columnHelper.accessor('hobby', {
cell: (info) => info.getValue()
}),
// Columns created with columnHelper.display won't be sortable.
// They need a header to be set manually since they're not just reading
// a property from the row.
columnHelper.display({
cell: (info) => <button>do something</button>,
header: 'Actions'
})
]
const data = [
{ name: 'chrissy', hobby: 'bare-knuckle boxing' },
{ name: 'agatha', hobby: 'crossfit' },
{ name: 'betty', hobby: 'acting' }
]

There are basically two ways to use DataTable to build a table from this config. The first uses the highest-level components that are bundled into DataTable to provide useful default behaviours with minimal code. The second directly accesses the state from DataTable and combines it with the Table UI components to achieve the same thing. This demonstrates how you could create more custom table UIs without the need to extend the high-level components.

With Defaults

The following two examples are exactly equivalent in their output. The second example is included to demonstrate what DataTable's subcomponents do: they bundle up UI components and logic to provide useful defaults. They exist at various levels of abstraction, e.g. DataTable.Table renders DataTable.Body, which renders DataTable.Row. This means that you can use whichever component provides useful functionality for your use case while still being low-level enough to let you combine it with your own custom logic.

<DataTable columns={columns} data={data}>
<DataTable.Table sortable css={{mb: '$4'}}/>
<DataTable.Pagination pageSize={5} />
</DataTable>
<DataTable columns={columns} data={data}>
<Table>
<DataTable.Head sortable />
<DataTable.Body />
</Table>
<DataTable.Pagination pageSize={5} />
</DataTable>

Rolling your own

If you need more flexibility than the default implementations provide, you can roll your own. Note that you can mix and match default implementations with your own. For example you could write your own table head implementation but use DataTable.Body for the body.

Note also that useDataTable can only be called by a child component of DataTable. In a real example, you'll probably have a separate named component which makes the useDataTable call, because if you're not using the defaults as above then you probably have some complex logic involved. In this example we've got an inline child component for simplicity.

Note that if you update the value of the data prop, it will reset the state of the table. This is useful if you are manipulating the data outside the context of the table.

<DataTable columns={columns} data={data}>
{() => {
const { getHeaderGroups, getRowModel, setGlobalFilter, getState } =
useDataTable()
const { globalFilter } = getState()
return (
<>
<Label htmlFor="search">User search</Label>
<SearchInput
name="search"
value={globalFilter}
onChange={setGlobalFilter}
/>
<Table>
<Table.Header>
{getHeaderGroups().map((headerGroup) => (
<Table.Row key={headerGroup.id}>
{headerGroup.headers.map((header) => {
const sort = header.column.getIsSorted()
return (
<Table.HeaderCell
onClick={header.column.getToggleSortingHandler()}
{...props}
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
{sort && { asc: '^', desc: 'v' }[sort as string]}
</Table.HeaderCell>
)
})}
</Table.Row>
))}
</Table.Header>
<Table.Body>
{getRowModel().rows.map((row) => (
<Table.Row>
{row.getVisibleCells().map((cell) => (
<Table.Cell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</Table.Cell>
))}
</Table.Row>
))}
</Table.Body>
</Table>
</>
// Then you could build your own pagination here too I guess? If you really wanted to?
)
}}
</DataTable>

Server-side pagination and sorting

DataTable can be used with local pagination or server-side pagination (getting only the data needed for the current page). To use one or the other, you have to use the data or getAsyncData prop respectively.

The getAsyncData function accepts an object with the necessary parameters to get the relevant piece of data for the current page and order. All the parameters are optional, with these defaults:

{
pageIndex: 0,
pageSize: 10,
sortBy: undefined,
sortDirection: undefined,
globalFilter: ''
}

The response from the getAsyncData function must match the following schema:

{
results: Array<Record<string, unknown>> // your current page data, sorted if specified
total: number // the total number of elements in your data
}

A loading state using <DataTable.Loading> is automatically included in DataTable which is visible while the getAsyncData promise is pending.

You can use DataTable.Error to display your own error component when the getAsyncData function promise rejects. Notice DataTable.Error doesn't render anything on its own, but whatever is passed as childern.

<DataTable
columns={columns}
defaultPageSize={10}
defaultSort={{ column: 'name', direction: 'asc' }}
initialState={{ pagination: { pageIndex: 0, pageSize: 10 } }}
getAsyncData={async ({
pageIndex,
pageSize,
sortBy,
sortDirection,
globalFilter
}) => {
const params = new URLSearchParams({
page: pageIndex,
pageSize,
order: sortBy,
dir: sortDirection,
search: globalFilter
})
const response = await fetch(`https://your-api?${params.toString()}`)
const { results, total } = await response.json()
return { results, total }
}}
>
<DataTable.Table sortable css={{ mb: '$4', minWidth: '500px' }} />
<DataTable.Error>
{(retry) => <Button onClick={retry}>Try again</Button>}
</DataTable.Error>
<DataTable.Pagination />
</DataTable>

DataTable.Error provides a retry function to the children which allows you to recall the getAsyncData function. The retry function can be called with all the paginated parameters as an optional object. If no parameters are provided, retry will be called with the last paginated options.

<DataTable.Error>
{(retry) => (
<Button
onClick={() =>
retry?.({
pageIndex: 5,
pageSize: 10,
sortBy: 'name',
sortDirection: 'asc',
globalFilter: ''
})
}
>
Retry
</Button>
)}
</DataTable.Error>

Features

Search

DataTable.Search renders a search input that filters the whole table by matching the input against values from any table column.

Sorting

A DataTable's data can be sorted by default and can also be sortable by the user. These two options are independent of each other.

Default sorting

DataTable takes an optional defaultSort prop to configure the column and direction for the table's default sorting, e.g. {column: 'name', direction: 'asc'}

User sorting

If DataTable's isSortable state is true, then DataTable.Header will be clickable to toggle between ascending, descending and no sorting in any sortable columns. DataTable.Head and DataTable.Table take an optional boolean sortable prop to configure this option.

Pagination

DataTable.Pagination can be passed as a child to DataTable to render the pagination UI and configure the parent DataTable to paginate its data. Note: there is currently a bug where DataTable will render all rows on the initial render, even when paginated. There is no visible effect, but it can cause performance issues on large tables. The workaround for this is to pass an initialState prop to DataTable. E.g. initialState={pagination: {pageSize: 5, pageIndex: 0}}. This will force pagination to apply properly on the initial render. Make sure the pagination prop matches the config you use in DataTable.Pagination, as the latter will take precedence after the initial render.

Drag and drop

The DataTable.DragAndDropTable can be rendered in place of DataTable.Table to allow users to reorder table rows via drag and drop. It takes an optional onDragAndDrop prop which is a function that fires when rows have been re-ordered via drag-and-drop. Use this to sync those changes with external data sources.

Note that column sorting conflicts with drag and drop behaviour. In any context where you allow drag and drop reordering, you probably want to disable column sorting (see User Sorting above). Similarly, you should probably disable pagination because users won't be able to drag rows across page boundaries.

Row IDs

Drag-and-drop functionality relies on each table row having a unique ID. DataTable.DragAndDropContainer will throw an error if your don't provide unique IDs for each row in the data provided to DataTable, so you should consider wrapping your table in an ErrorBoundary to reduce the impact on your user if there is a problem with your data. By default, DataTable.DragAndDropContainer will look for this id in an id property on each object in data. You can use the idColumn prop to provide the name of a different property, e.g. userId, that already exists on your data so that you don't have to generate new IDs just for the table. For example, you could provide data like this with no additional configuration:

const data = [
{ name: 'chrissy', hobby: 'bare-knuckle boxing', id: 1 },
{ name: 'agatha', hobby: 'crossfit', id: 2 },
{ name: 'betty', hobby: 'acting', id: 3 }
]`
<DataTable data={data} columns={columns}>
<DataTable.DragAndDropTable onDragAndDrop={(oldIndex, newIndex, newData) => console.log(oldIndex, newIndex, newData)}/>
</DataTable>

Or you could provide this data and specify the id column accordingly:

const data = [
{ name: 'chrissy', hobby: 'bare-knuckle boxing', userId: 1 },
{ name: 'agatha', hobby: 'crossfit', userId: 2 },
{ name: 'betty', hobby: 'acting', userId: 3 }
]`
<DataTable data={data} columns={columns}>
<DataTable.DragAndDropTable idColumn="userId" onDragAndDrop={(oldIndex, newIndex, newData) => console.log(oldIndex, newIndex, newData)} />
</DataTable>

API Reference

DataTable

PropTypeDefaultRequired
columnsany

-

defaultSort
TDefaultSort

-

-

initialState
Partial<VisibilityTableState & ColumnOrderTableState & ColumnPinningTableState & FiltersTableState & ... 5 more ... & RowSelectionTableState>

-

-

data
TableData

-

-

getAsyncData
TGetAsyncData

-

-

DataTable.Table

PropTypeDefaultRequired
size
"md" | "lg"

-

-

corners
"round" | "square"

-

-

cssCSSProperties

-

-

theme
string | number | `${number}`

-

-

sortable
boolean

-

-

striped
boolean

-

-

DataTable.Head

PropTypeDefaultRequired
cssCSSProperties

-

-

theme
string | number | `${number}`
light

-

sortable
boolean

-

DataTable.HeaderCell

PropTypeDefaultRequired
cssCSSProperties

-

-

headerHeader<Record<string, unknown>, unknown>

-

DataTable.Body

PropTypeDefaultRequired
cssCSSProperties

-

-

striped
boolean

-

DataTable.Row

PropTypeDefaultRequired
cssCSSProperties

-

-

rowRow<Record<string, unknown>>

-

DataTable.DataCell

PropTypeDefaultRequired
cellCell<Record<string, unknown>, unknown>

-

DataTable.GlobalFilter

PropTypeDefaultRequired
size
"sm" | "md"

-

-

state
"error"

-

-

cssCSSProperties

-

-

namestring

-

asJSX.IntrinsicElements

-

-

type
"number" | "text" | "search" | "email" | "password" | "tel" | "url"

-

-

clearText
string

-

-

labelstring

-

hideLabel
boolean

-

DataTable.Loading

PropTypeDefaultRequired
size
"sm" | "md" | "lg"

-

-

message
string

-

-

cssCSSProperties

-

-

DataTable.Error

PropTypeDefaultRequired