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.

PropertyTypeDescription
tableColumnsTableColumn<T>[]Decorated columns in current order
tableGroupsColumnGroup[]Column groups in current order
defaultSortColumnTableColumn<T>The column matching defaultSortFieldId. Pass it to useTableState
defaultSortDirectionSortOrder'asc' or 'desc' from defaultSortAsc
columnDragColumnDragSliceDrag-to-reorder event handlers (see below)
draggingColumnIdstringID of the column being dragged, or ''
draggingGroupKeystringKey of the group being dragged, or ''
handlePinColumn(columnId, side?: 'left' | 'right') => voidPin a column to a side, or unpin it by omitting side
handleHideColumn(columnId) => voidSet omit: true on a column
handleResetColumns() => voidRestore 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:

PropTypeDefaultDescription
dataT[]requiredFull dataset (used for selection total counts)
keyFieldstring'id'Unique row identifier field
defaultSortColumnTableColumn<T>no sortColumn active on first render, from useColumns
defaultSortDirectionSortOrder'asc''asc' | 'desc'
paginationbooleanfalseOnly affects page recalculation in server mode. Page state is always tracked
paginationDefaultPagenumber1Starting page (1-based)
paginationPerPagenumber10Rows per page
paginationPagenumberControlled current page
paginationServerbooleanfalseSkip internal page math
paginationServerOptionsPaginationServerOptions{}persistSelectedOnPageChange, persistSelectedOnSort
paginationTotalRowsnumber0Total server rows (server mode only)
paginationResetDefaultPagebooleanfalseToggle to reset to page 1
selectableRowsSinglebooleanfalseAllow only one selected row
selectableRowsVisibleOnlybooleanfalse"Select all" acts on current page only
selectableRowSelected(row: T) => boolean | nullnullPre-select matching rows
controlledSelectedRowsT[]Controlled selection. Overrides internal selection state
clearSelectedRowsbooleanfalseToggle to clear selection
onSelectedRowsChange(state) => voidFires on any selection change
onChangePage(page, total) => voidFires after page changes
onChangeRowsPerPage(perPage, page) => voidFires after rows-per-page changes

To hear about sort changes, pass onSort to useTableData, which is the hook that holds the sorted rows.

Returns:

PropertyTypeDescription
tableStateTableState<T>Full state snapshot (see below)
handleSort(action: SortAction<T>) => voidDispatch a sort change
handleSelectAllRows(action: AllRowsAction<T>) => voidDispatch select/deselect all
handleSelectedRow(action: SingleRowAction<T>) => voidDispatch a single row toggle
handleSelectedRange(action: RangeRowAction<T>) => voidDispatch a Shift-click range selection
handleChangePage(page: number) => voidDispatch a page change
handleChangeRowsPerPage(perPage, rowCount) => voidDispatch a per-page change
handleClearSelectedRows() => voidProgrammatically clear all selections
handleClearSort() => voidProgrammatically 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:

FieldType
currentPagenumber
rowsPerPagenumber
selectedColumnTableColumn<T>
sortDirectionSortOrder
sortColumnsSortColumn<T>[]: full sort config in priority order; required by useTableData
selectedRowsT[]
selectedCountnumber
allSelectedboolean

useTableData<T>

function useTableData<T>(props: UseTableDataProps<T>): UseTableDataReturn<T>;
PropTypeDefaultDescription
dataT[]requiredSource rows (full array for client-side; current-page array for server-side)
selectedColumnTableColumn<T>requiredFrom tableState.selectedColumn
sortDirectionSortOrderrequiredFrom tableState.sortDirection
sortColumnsSortColumn<T>[]requiredFrom tableState.sortColumns
currentPagenumberrequiredFrom tableState.currentPage
rowsPerPagenumberrequiredFrom tableState.rowsPerPage
paginationbooleanfalseSlice tableRows to the current page
paginationServerbooleanfalseSkip page slicing (server already paginated)
sortServerbooleanfalseSkip sorting (server already sorted)
sortFunctionSortFunction<T> | nullnullGlobal custom sort comparator
onSort(col, dir, rows, sortColumns) => voidCalled after sort; receives sorted rows
PropertyTypeDescription
sortedDataT[]All rows after sorting (full dataset, pre-slice)
tableRowsT[]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 form useColumnFilter(columns, filterValues, onFilterChange) still works and is deprecated. It cannot supply rows, so set filters need the options object. The positional form is removed in v9.
PropertyTypeDescription
filterValuesRecord<string | number, FilterState>Current filter state per column ID
handleFilterChange(columnId, filter) => voidCall 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;
};
OptionTypeDescription
cellNavigationbooleanMaster on/off switch. Default false, matching the cellNavigation prop, so pass true to enable. When off, activeCell is always null.
effectiveColumnsTableColumn<T>[]Required. Your current column list. Columns with omit: true are excluded from the nav grid.
filteredTableRowCountnumberRequired. Row count for the currently visible page. Clamps ArrowDown/End.
showTableHeadbooleanWhether a header row participates in the grid (row -1), default true. false removes header cells from the tab sequence entirely.
selectableRowsbooleanReserves nav column 0 for a selection checkbox, if you render one. Default false.
expandableRowsbooleanReserves the next nav column for an expander button, if you render one. Default false.
expandableRowsHideExpanderbooleanSkip 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;
};
OptionTypeDescription
initialColumnWidthsRecord<string | number, number>Seed widths, e.g. hydrated from localStorage.
onColumnResize(columnId, width, allWidths) => voidFires on pointer release with the settled width.
isRTLbooleanInvert 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

OptionTypeDefaultDescription
columnsTableColumn<T>[]requiredSame columns array you pass to DataTable. Columns with omit: true are skipped.
rowsT[]requiredRows 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.
headerOverridesRecord<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

PropertyTypeDescription
toCSV() => stringBuild a CSV string (no download).
toJSON() => stringBuild a pretty-printed JSON array string.
download(filename: string, format?: 'csv' | 'json') => voidTrigger 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.