Hook reference
Signatures, options, and return values for everything exported from
react-data-table-component/headless. For what the hooks are and how they fit together,
start with the Headless overview. For a complete styled table,
see Headless table with Tailwind.
import {
useColumns,
useTableState,
useTableData,
useColumnFilter,
useColumnVisibility,
useColumnResize,
useCellNavigation,
useTableExport,
getPinnedOffsets,
getPinnedTotalWidths,
getPinnedCellMeta,
} from 'react-data-table-component/headless';
Option and result types are exported under matching names, such as UseTableStateProps and
UseTableStateReturn. See TypeScript for the full
list. useColumnVisibility is documented on
the Column visibility page.
useColumns<T>
function useColumns<T>(
columns: TableColumn<T>[],
options?: {
onColumnOrderChange?: (nextOrder: TableColumn<T>[]) => void;
onColumnGroupOrderChange?: (nextGroups: ColumnGroup[], nextColumns: TableColumn<T>[]) => void;
columnGroups?: ColumnGroup[];
defaultSortFieldId?: string | number | null;
defaultSortAsc?: boolean; // default true
},
): UseColumnsResult<T>;
Every option is optional. columnGroups and onColumnGroupOrderChange only matter
if you're using column groups. The positional form
useColumns(columns, onColumnOrderChange, onColumnGroupOrderChange, columnGroups, defaultSortFieldId, defaultSortAsc)
still works.
| Property | Type | Description |
|---|---|---|
tableColumns | TableColumn<T>[] | Decorated columns in current order |
tableGroups | ColumnGroup[] | Column groups in current order |
defaultSortColumn | TableColumn<T> | The column matching defaultSortFieldId. Pass it to useTableState |
defaultSortDirection | SortOrder | 'asc' or 'desc' from defaultSortAsc |
columnDrag | ColumnDragSlice | Drag-to-reorder event handlers (see below) |
draggingColumnId | string | ID of the column being dragged, or '' |
draggingGroupKey | string | Key of the group being dragged, or '' |
handlePinColumn | (columnId, side?: 'left' | 'right') => void | Pin a column to a side, or unpin it by omitting side |
handleHideColumn | (columnId) => void | Set omit: true on a column |
handleResetColumns | () => void | Restore the original order, pins, and visibility |
For drag-to-reorder, give each header cell a data-column-id attribute and
draggable, then attach the handlers from columnDrag. The hook reads the column
id from that attribute.
const { tableColumns, columnDrag, draggingColumnId } = useColumns(columns);
{tableColumns.map(col => (
<th
key={col.id}
data-column-id={col.id}
draggable
onDragStart={columnDrag.onDragStart}
onDragEnter={columnDrag.onDragEnter}
onDragOver={columnDrag.onDragOver}
onDragLeave={columnDrag.onDragLeave}
onDragEnd={columnDrag.onDragEnd}
onPointerDown={columnDrag.onPointerDown} // touch and pen
style={{ opacity: draggingColumnId === String(col.id) ? 0.5 : 1 }}
>
{col.name}
</th>
))}
All five drag handlers must be attached for mouse reordering to work. onPointerDown adds
long-press reordering on touch and pen. The onGroup* handlers in the same object do the
same for group header cells, keyed by a data-group-key attribute.
useTableState<T>
function useTableState<T>(props: UseTableStateProps<T>): UseTableStateReturn<T>;
Only data is required. UseTableStateProps and UseTableStateReturn
are exported types.
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
data | T[] | required | Full dataset (used for selection total counts) |
keyField | string | 'id' | Unique row identifier field |
defaultSortColumn | TableColumn<T> | no sort | Column active on first render, from useColumns |
defaultSortDirection | SortOrder | 'asc' | 'asc' | 'desc' |
pagination | boolean | false | Only affects page recalculation in server mode. Page state is always tracked |
paginationDefaultPage | number | 1 | Starting page (1-based) |
paginationPerPage | number | 10 | Rows per page |
paginationPage | number | Controlled current page | |
paginationServer | boolean | false | Skip internal page math |
paginationServerOptions | PaginationServerOptions | {} | persistSelectedOnPageChange, persistSelectedOnSort |
paginationTotalRows | number | 0 | Total server rows (server mode only) |
paginationResetDefaultPage | boolean | false | Toggle to reset to page 1 |
selectableRowsSingle | boolean | false | Allow only one selected row |
selectableRowsVisibleOnly | boolean | false | "Select all" acts on current page only |
selectableRowSelected | (row: T) => boolean | null | null | Pre-select matching rows |
controlledSelectedRows | T[] | Controlled selection. Overrides internal selection state | |
clearSelectedRows | boolean | false | Toggle to clear selection |
onSelectedRowsChange | (state) => void | Fires on any selection change | |
onChangePage | (page, total) => void | Fires after page changes | |
onChangeRowsPerPage | (perPage, page) => void | Fires after rows-per-page changes |
To hear about sort changes, pass onSort to useTableData, which is the hook
that holds the sorted rows.
Returns:
| Property | Type | Description |
|---|---|---|
tableState | TableState<T> | Full state snapshot (see below) |
handleSort | (action: SortAction<T>) => void | Dispatch a sort change |
handleSelectAllRows | (action: AllRowsAction<T>) => void | Dispatch select/deselect all |
handleSelectedRow | (action: SingleRowAction<T>) => void | Dispatch a single row toggle |
handleSelectedRange | (action: RangeRowAction<T>) => void | Dispatch a Shift-click range selection |
handleChangePage | (page: number) => void | Dispatch a page change |
handleChangeRowsPerPage | (perPage, rowCount) => void | Dispatch a per-page change |
handleClearSelectedRows | () => void | Programmatically clear all selections |
handleClearSort | () => void | Programmatically reset sort to the default |
SortAction<T> is { type: 'SORT_CHANGE', selectedColumn: TableColumn<T>,
clearSelectedOnSort: boolean, additive: boolean, defaultSortDirection: SortOrder }.
additive: true adds the column to the existing sort instead of replacing it (multi-column
sort); most callers pass false. Add direction: SortOrder to force a direction
instead of cycling.
Selection uses the same pattern: build a SELECT_SINGLE_ROW, SELECT_ALL_ROWS, or
SELECT_RANGE action. The action types (SortAction, SingleRowAction,
AllRowsAction, RangeRowAction) and TableState are exported.
const { tableState, handleSelectedRow } = useTableState({ data });
const isSelected = (row: Person) => tableState.selectedRows.some(r => r.id === row.id);
<input
type="checkbox"
checked={isSelected(row)}
onChange={() =>
handleSelectedRow({
type: 'SELECT_SINGLE_ROW',
keyField: 'id',
row,
isSelected: isSelected(row), // the state before this click
rowCount: data.length,
singleSelect: false,
})
}
/>
TableState<T> shape:
| Field | Type |
|---|---|
currentPage | number |
rowsPerPage | number |
selectedColumn | TableColumn<T> |
sortDirection | SortOrder |
sortColumns | SortColumn<T>[]: full sort config in priority order; required by useTableData |
selectedRows | T[] |
selectedCount | number |
allSelected | boolean |
useTableData<T>
function useTableData<T>(props: UseTableDataProps<T>): UseTableDataReturn<T>;
| Prop | Type | Default | Description |
|---|---|---|---|
data | T[] | required | Source rows (full array for client-side; current-page array for server-side) |
selectedColumn | TableColumn<T> | required | From tableState.selectedColumn |
sortDirection | SortOrder | required | From tableState.sortDirection |
sortColumns | SortColumn<T>[] | required | From tableState.sortColumns |
currentPage | number | required | From tableState.currentPage |
rowsPerPage | number | required | From tableState.rowsPerPage |
pagination | boolean | false | Slice tableRows to the current page |
paginationServer | boolean | false | Skip page slicing (server already paginated) |
sortServer | boolean | false | Skip sorting (server already sorted) |
sortFunction | SortFunction<T> | null | null | Global custom sort comparator |
onSort | (col, dir, rows, sortColumns) => void | Called after sort; receives sorted rows |
| Property | Type | Description |
|---|---|---|
sortedData | T[] | All rows after sorting (full dataset, pre-slice) |
tableRows | T[] | With pagination: true, the current page of sortedData. Otherwise the same rows as sortedData |
When you also filter, pass sortedData (not a paginated tableRows) to
useColumnFilter's filteredData() and slice the page afterwards. See
Filter before you paginate.
useColumnFilter<T>
import type { FilterState } from 'react-data-table-component';
function useColumnFilter<T>(
columns: TableColumn<T>[],
options?: {
filterValues?: Record<string | number, FilterState>;
onFilterChange?: (columnId: string | number, filter: FilterState) => void;
localization?: Localization['filter'];
rows?: T[];
},
): UseColumnFilterResult<T>;
Omit filterValues and onFilterChange to run in uncontrolled mode
(internal state). Pass both to run in controlled mode (you own the state).
Pass rows when any column uses filterType: 'set'. That is the row array the checklist
derives its distinct values from, so without it a set filter opens an empty list. Every other filter type ignores
it.
The older positional formuseColumnFilter(columns, filterValues, onFilterChange)still works and is deprecated. It cannot supplyrows, so set filters need the options object. The positional form is removed in v9.
| Property | Type | Description |
|---|---|---|
filterValues | Record<string | number, FilterState> | Current filter state per column ID |
handleFilterChange | (columnId, filter) => void | Call this when the user applies a filter |
filteredData | (data: T[]) => T[] | Apply all active filters to a row array |
filteredData is a stable memoized function, safe to call in render.
Each column can define filterFunction: (row: T, filter: FilterState) => boolean to
override the built-in operator logic. See Filtering for the full
FilterState type and operator reference.
useCellNavigation<T>
Powers the WAI-ARIA grid pattern behind cellNavigation: arrow keys, Home/End,
Ctrl+Home/Ctrl+End, and a roving tabindex. It reads and writes plain
data-nav-row / data-nav-col attributes on your own cells. It doesn't know or
care what markup they're attached to.
function useCellNavigation<T>(options: UseCellNavigationOptions<T>): {
activeCell: ActiveCell | null;
handleNavFocus: (e: React.FocusEvent<HTMLDivElement>) => void;
handleNavKeyDown: (e: React.KeyboardEvent<HTMLDivElement>) => void;
};
| Option | Type | Description |
|---|---|---|
cellNavigation | boolean | Master on/off switch. Default false, matching the cellNavigation prop, so pass true to enable. When off, activeCell is always null. |
effectiveColumns | TableColumn<T>[] | Required. Your current column list. Columns with omit: true are excluded from the nav grid. |
filteredTableRowCount | number | Required. Row count for the currently visible page. Clamps ArrowDown/End. |
showTableHead | boolean | Whether a header row participates in the grid (row -1), default true. false removes header cells from the tab sequence entirely. |
selectableRows | boolean | Reserves nav column 0 for a selection checkbox, if you render one. Default false. |
expandableRows | boolean | Reserves the next nav column for an expander button, if you render one. Default false. |
expandableRowsHideExpander | boolean | Skip the expander column reservation even when expandableRows is true. Default false. |
The hook supplies focus movement only. Set role="grid", role="row", and
role="gridcell" (or use native table elements) on your own markup.
Wire it up by spreading data-nav-row/data-nav-col (row -1 for the
header) onto each focusable cell, setting tabIndex from activeCell, and
attaching onFocus/onKeyDown to the table root:
function Row({ row, rowIndex, columns, activeCell }) {
return (
<tr>
{columns.map((col, colIndex) => (
<td
key={col.id}
data-nav-row={rowIndex}
data-nav-col={colIndex}
tabIndex={activeCell?.row === rowIndex && activeCell?.col === colIndex ? 0 : -1}
>
{col.selector?.(row, rowIndex)}
</td>
))}
</tr>
);
}
function MyTable({ data, columns }) {
const { activeCell, handleNavFocus, handleNavKeyDown } = useCellNavigation({
cellNavigation: true,
effectiveColumns: columns,
filteredTableRowCount: data.length,
showTableHead: false, // this table renders no header row
});
return (
<table onFocus={handleNavFocus} onKeyDown={handleNavKeyDown}>
<tbody>
{data.map((row, i) => (
<Row key={row.id} row={row} rowIndex={i} columns={columns} activeCell={activeCell} />
))}
</tbody>
</table>
);
}
Cells that hold an interactive widget instead of plain text (a checkbox, an expander button) should
also set data-nav-widget="true", and the hook then focuses the widget itself rather than
the cell wrapper, so Space/Enter activate it natively. See
Keyboard navigation for the full key reference this
reproduces.
useColumnResize
function useColumnResize(options?: UseColumnResizeOptions): {
columnWidths: Record<string | number, number>;
handleResizeStart: (columnId: string | number, e: React.PointerEvent, minWidth?: string) => void;
};
| Option | Type | Description |
|---|---|---|
initialColumnWidths | Record<string | number, number> | Seed widths, e.g. hydrated from localStorage. |
onColumnResize | (columnId, width, allWidths) => void | Fires on pointer release with the settled width. |
isRTL | boolean | Invert the drag direction for right-to-left layouts. |
Attach handleResizeStart to a drag handle's onPointerDown, inside a header cell
that carries a data-column-id attribute (the hook measures that cell for the starting
width). It tracks pointermove/pointerup on document itself, so
you don't need to wire those up. The optional third argument is a CSS min width such as
'80px'; the floor is 40px. Apply columnWidths[column.id] as an inline width on your header
and body cells for that column.
Pinning: getPinnedOffsets, getPinnedTotalWidths, getPinnedCellMeta
Column pinning has no hook. There's no state to own beyond column.pinned on your own
column list. These are pure functions: given your columns and current widths, they compute sticky
offsets and shadow metadata.
function getPinnedOffsets<T>(
columns: TableColumn<T>[],
columnWidths: Record<string | number, number>,
selectableRows: boolean,
expandableRows: boolean,
expandableRowsHideExpander: boolean,
): PinnedOffsets;
function getPinnedTotalWidths<T>(
columns: TableColumn<T>[],
columnWidths: Record<string | number, number>,
selectableRows: boolean,
expandableRows: boolean,
expandableRowsHideExpander: boolean,
): { left: number; right: number };
function getPinnedCellMeta<T>(
column: TableColumn<T>,
pinnedOffsets: PinnedOffsets | undefined,
zIndex?: number,
): PinnedCellMeta;
const pinnedOffsets = getPinnedOffsets(columns, columnWidths, false, false, false);
function Cell({ column, pinnedOffsets }: { column: TableColumn<Row>; pinnedOffsets: PinnedOffsets }) {
const meta = getPinnedCellMeta(column, pinnedOffsets, 1);
return (
<td className={meta.className} style={meta.style}>
{/* ... */}
</td>
);
}
getPinnedCellMeta returns a style object (position: 'sticky',
the computed left/right offset, and zIndex) that's enough to
render pinned columns on its own. className adds rdt_pinLeftLast /
rdt_pinRightFirst on the pinned boundary's edge column. Those classes only draw a
shadow if you import react-data-table-component/css (or match the selectors
yourself); they're no-ops otherwise. See Column pinning for the
column.pinned API these functions read.
useTableExport
Generates CSV or JSON from any set of columns and rows. Pair it with
useTableData and useColumnFilter to export exactly what the
user sees (sorted, filtered, and paginated) rather than the raw dataset.
function useTableExport<T>(options: UseTableExportOptions<T>): UseTableExportResult;
Options
| Option | Type | Default | Description |
|---|---|---|---|
columns | TableColumn<T>[] | required | Same columns array you pass to DataTable. Columns with omit: true are skipped. |
rows | T[] | required | Rows to export. Pass the filtered and sorted slice to export the current view. |
valueSource | 'selector' | 'format' | 'selector' | 'selector' exports raw selector values; 'format' runs column.format first. |
headerOverrides | Record<string | number, string> | - | Replace the header label per column id. Useful when column.name is a React node. |
columnOrder | (string | number)[] | - | Restrict and reorder exported columns by id. Omit to include all visible columns. |
Return value
| Property | Type | Description |
|---|---|---|
toCSV | () => string | Build a CSV string (no download). |
toJSON | () => string | Build a pretty-printed JSON array string. |
download | (filename: string, format?: 'csv' | 'json') => void | Trigger a browser download. format defaults to 'csv'. No-op on the server. |
copy | (format?: 'csv' | 'json') => Promise<void> | Copy to the clipboard. Rejects when the Clipboard API is unavailable. |
Example: export the current filtered view
import {
useColumns,
useTableState,
useTableData,
useColumnFilter,
useTableExport,
type TableColumn,
} from 'react-data-table-component';
const columns: TableColumn<Employee>[] = [
{ id: 'name', name: 'Name', selector: r => r.name },
{ id: 'dept', name: 'Department', selector: r => r.department },
{ id: 'salary', name: 'Salary', selector: r => r.salary },
];
function MyTable({ data }: { data: Employee[] }) {
const { tableColumns } = useColumns(columns, () => {}, undefined, undefined, null, true);
const { tableState } = useTableState({ data, keyField: 'id' /* … */ });
const { tableRows } = useTableData({ data, columns: tableColumns /* … */ });
const { filterValues, handleFilterChange, filteredData } = useColumnFilter(tableColumns);
const rows = filteredData(tableRows);
const { download, copy } = useTableExport({ columns: tableColumns, rows });
return (
<div>
<button onClick={() => download('employees.csv')}>Download CSV</button>
<button onClick={() => download('employees.json', 'json')}>Download JSON</button>
<button onClick={() => copy()}>Copy to clipboard</button>
{/* … your table markup … */}
</div>
);
}
See the Export (CSV / JSON) page for a full worked example including
integration with the <DataTable /> component's onExport callback.