Headless hooks

react-data-table-component ships four hooks that power <DataTable /> internally. You can import them directly and compose them yourself when you need full control over markup and styles, without giving up sorting, pagination, selection, filtering, and column-drag logic.

This is the middle ground between using the styled <DataTable /> as-is and rebuilding everything from scratch with a fully headless library.


When to use this

  • You have a design system and can't override the default styles far enough
  • You're building inside a heavily customised UI (e.g. a design system component library)
  • You want the table logic but need to render it inside something other than a standard <table> or flex layout
  • You want to compose only some hooks (e.g. sorting + pagination) and handle the rest yourself

If the default <DataTable /> with customStyles covers your needs, use that. It is less work.


The data pipeline

Each hook owns one layer of the pipeline. Data flows top-to-bottom; state flows back up via callbacks. Understanding this makes it straightforward to drop any layer and replace it with your own logic.

%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#f0f4ff", "primaryTextColor": "#1e293b", "primaryBorderColor": "#6366f1", "lineColor": "#6366f1", "secondaryColor": "#f8fafc", "tertiaryColor": "#f1f5f9"}, "layout": {"algorithm": "elk"}}}%%
flowchart TD
  A(["Your data[]"])

  subgraph pipeline ["Hook pipeline"]
    direction TB
    B["useColumns
    ─────────────────
    • decorates column ids
    • resolves defaultSortFieldId
    • manages drag-to-reorder"]

    C["useTableState
    ─────────────────
    • currentPage
    • rowsPerPage
    • sortDirection
    • selectedRows"]

    D["useTableData
    ─────────────────
    • sorts client-side
    • paginates + slices
    • (skipped in server mode)"]

    E["useColumnFilter (optional)
    ─────────────────
    • per-column filter state
    • filteredData() helper"]
  end

  F(["Rendered rows"])

  A --> B
  A --> C
  B --> C
  C --> D
  D --> E
  E --> F

Component tree

When you use <DataTable />, it renders the following structure. Each node has a stable CSS class so you can target it from your stylesheet. When you use the hooks directly, you own this tree entirely.

%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#f0f4ff", "primaryTextColor": "#1e293b", "primaryBorderColor": "#6366f1", "lineColor": "#6366f1", "secondaryColor": "#f8fafc"}, "layout": "elk"}}%%
graph TD
    W["div.rdt_wrapper (root)"]
    H["Header (optional title + actions)"]
    S["Subheader (optional: filters, search)"]
    SC["div.rdt_responsiveWrapper (scroll container)"]
    T["div.rdt_table  aria role=table"]
    TH["div.rdt_TableHead"]
    THR["div.rdt_TableHeadRow"]
    TC["div.rdt_TableCol  ×N columns
    data-column-id=..."]
    RH["span.rdt_columnSortable (sort handle + label)
    + div.rdt_resizeHandle (when resizable)"]
    TB["div.rdt_TableBody"]
    TR["div.rdt_row  ×N rows
    id=row-..."]
    TD["div.rdt_TableCell  ×N columns
    data-column-id=..."]
    CEL["div[data-tag=allowRowEvents]
    cell content"]
    PS["PinnedScrollbar (when row pinning enabled)"]
    PG["Pagination (when pagination enabled)"]

    W --> H
    W --> S
    W --> SC
    SC --> T
    T --> TH
    T --> TB
    TH --> THR
    THR --> TC
    TC --> RH
    TB --> TR
    TR --> TD
    TD --> CEL
    W --> PS
    W --> PG

The four hooks

HookWhat it does
useColumnsManages column order and drag-to-reorder
useTableStateSort, pagination, and row-selection state + dispatchers
useTableDataSorts and paginates the raw data into renderable rows
useColumnFilterPer-column filter values and client-side filter logic

All four are pure: no JSX, no CSS, no hard dependency on the rendered component tree.


Basic example

import {
  useColumns,
  useTableState,
  useTableData,
  useColumnFilter,
} from 'react-data-table-component';

const columns = [
  { id: 1, name: 'Name',  selector: (row) => row.name,  sortable: true },
  { id: 2, name: 'Email', selector: (row) => row.email },
  { id: 3, name: 'Role',  selector: (row) => row.role },
];

function MyTable({ data }) {
  // 1. Column order + drag state
  const { tableColumns, handleDragStart, handleDragEnd } = useColumns(
    columns,
    () => {},   // onColumnOrderChange
    null,       // defaultSortFieldId
    true,       // defaultSortAsc
  );

  // 2. Sort / page / selection state
  const {
    tableState,
    handleSort,
    handleChangePage,
    handleChangeRowsPerPage,
    handleSelectedRow,
    handleSelectAllRows,
  } = useTableState({
    data,
    keyField: 'id',
    defaultSortColumn: tableColumns[0],
    defaultSortDirection: 'asc',
    paginationDefaultPage: 1,
    paginationPerPage: 10,
    paginationServer: false,
    paginationServerOptions: {},
    paginationTotalRows: 0,
    pagination: true,
    selectableRowsSingle: false,
    selectableRowsVisibleOnly: false,
    selectableRowSelected: null,
    clearSelectedRows: false,
    paginationResetDefaultPage: false,
    onSelectedRowsChange: () => {},
    onSort: () => {},
    onChangePage: () => {},
    onChangeRowsPerPage: () => {},
  });

  const { selectedColumn, sortDirection, currentPage, rowsPerPage } = tableState;

  // 3. Sorted + paginated rows
  const { sortedData, tableRows } = useTableData({
    data,
    columns: tableColumns,
    selectedColumn,
    sortDirection,
    currentPage,
    rowsPerPage,
    pagination: true,
    paginationServer: false,
    sortServer: false,
    sortFunction: null,
    onSort: () => {},
  });

  // 4. Optional: per-column filtering (uncontrolled — internal state)
  const { filteredData } = useColumnFilter(tableColumns);

  const rows = filteredData(tableRows);

  return (
    <div>
      <table>
        <thead>
          <tr>
            {tableColumns.map(col => (
              <th key={col.id} onClick={() => handleSort({ type: 'SORT_CHANGE', sortColumn: col, clearSelectedRows: false })}>
                {col.name}
              </th>
            ))}
          </tr>
        </thead>
        <tbody>
          {rows.map((row, i) => (
            <tr key={i}>
              {tableColumns.map(col => (
                <td key={col.id}>{col.selector?.(row, i)}</td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

Hook signatures

useColumns<T>

function useColumns<T>(
  columns: TableColumn<T>[],
  onColumnOrderChange: (nextOrder: TableColumn<T>[]) => void,
  defaultSortFieldId: string | number | null | undefined,
  defaultSortAsc: boolean,
): ColumnsHook<T>
PropertyTypeDescription
tableColumnsTableColumn<T>[]Decorated columns in current drag order
draggingColumnIdstringID of the column being dragged, or ''
defaultSortColumnTableColumn<T>The column matching defaultSortFieldId
defaultSortDirectionSortOrder'asc' or 'desc' from defaultSortAsc
handleDragStartDragEventHandlerAttach to onDragStart on the header cell
handleDragEnterDragEventHandlerAttach to onDragEnter on the header cell
handleDragOverDragEventHandlerAttach to onDragOver on the header cell
handleDragLeaveDragEventHandlerAttach to onDragLeave on the header cell
handleDragEndDragEventHandlerAttach to onDragEnd on the header cell

All five drag handlers must be attached for reordering to work correctly.


useTableState<T>

function useTableState<T>(props: UseTableStateProps<T>): UseTableStateReturn<T>

Props:

PropTypeDescription
dataT[]Full dataset (used for selection total counts)
keyFieldstringUnique row identifier field
defaultSortColumnTableColumn<T>Column active on first render
defaultSortDirectionSortOrder'asc' | 'desc'
paginationbooleanEnable pagination state
paginationDefaultPagenumberStarting page (1-based)
paginationPerPagenumberRows per page
paginationServerbooleanSkip internal page math
paginationServerOptionsPaginationServerOptionspersistSelectedOnPageChange, persistSelectedOnSort
paginationTotalRowsnumberTotal server rows (server mode only)
paginationResetDefaultPagebooleanToggle to reset to page 1
selectableRowsSinglebooleanAllow only one selected row
selectableRowsVisibleOnlyboolean"Select all" acts on current page only
selectableRowSelected(row: T) => boolean | nullPre-select matching rows
clearSelectedRowsbooleanToggle to clear selection
onSelectedRowsChange(state) => voidFires on any selection change
onSort(col, dir, rows) => voidFires after sort state changes
onChangePage(page, total) => voidFires after page changes
onChangeRowsPerPage(perPage, page) => voidFires after rows-per-page changes

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
handleChangePage(page: number) => voidDispatch a page change
handleChangeRowsPerPage(perPage, rowCount) => voidDispatch a per-page change
handleClearSelectedRows() => voidProgrammatically clear all selections

TableState<T> shape:

FieldType
currentPagenumber
rowsPerPagenumber
selectedColumnTableColumn<T>
sortDirectionSortOrder
selectedRowsT[]
selectedCountnumber
allSelectedboolean

useTableData<T>

function useTableData<T>(props: UseTableDataProps<T>): UseTableDataReturn<T>
PropTypeDescription
dataT[]Source rows (full array for client-side; current-page array for server-side)
columnsTableColumn<T>[]From useColumns
selectedColumnTableColumn<T>From tableState.selectedColumn
sortDirectionSortOrderFrom tableState.sortDirection
currentPagenumberFrom tableState.currentPage
rowsPerPagenumberFrom tableState.rowsPerPage
paginationbooleanEnable page slicing
paginationServerbooleanSkip page slicing (server already paginated)
sortServerbooleanSkip sorting (server already sorted)
sortFunctionSortFunction<T> | nullGlobal custom sort comparator
onSort(col, dir, rows) => voidCalled after sort; receives sorted rows
PropertyTypeDescription
sortedDataT[]All rows after sorting (full dataset, pre-slice)
tableRowsT[]Rows for the current page after sort + slice

Pass tableRows (not sortedData) to useColumnFilter.filteredData() and then to your render.


useColumnFilter<T>

import type { FilterState } from 'react-data-table-component';

function useColumnFilter<T>(
  columns: TableColumn<T>[],
  controlledFilterValues?: Record<string | number, FilterState>,
  onFilterChange?: (columnId: string | number, filter: FilterState) => void,
): UseColumnFilterResult<T>

Omit both optional arguments to run in uncontrolled mode (internal state). Pass both to run in controlled mode (you own the state).

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 memoised 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.


How useTableState and useTableData relate

useTableState owns the sort/page/selection state. useTableData takes that state as inputs and returns the derived rows to render. They are intentionally separate so you can skip useTableData entirely if you're handling sorting and pagination server-side.

// Server-side: you manage the rows, just use useTableState for UI state
const { tableState, handleSort, handleChangePage } = useTableState({ ..., paginationServer: true });

// When tableState changes, fire your own API call. No useTableData needed.
useEffect(() => {
  fetchPage(tableState.currentPage, tableState.rowsPerPage, tableState.sortDirection);
}, [tableState.currentPage, tableState.rowsPerPage, tableState.sortDirection]);

State ownership

StateOwned byControlled prop
Current pageuseTableStatepaginationDefaultPage + onChangePage
Rows per pageuseTableStatepaginationPerPage + onChangeRowsPerPage
Selected rowsuseTableStateselectableRowSelected + onSelectedRowsChange
Sort column + directionuseTableStatedefaultSortFieldId + onSort
Column filtersuseColumnFilterfilterValues + onFilterChange
Column widthsuseColumnResizeNone — DOM-only (see recipes)
Column orderuseColumnsonColumnOrderChange
Expanded rowslocal to each row componentexpandableRowExpanded + onRowExpandToggled

What you're responsible for

When you use the hooks directly, you own:

  • Markup: <table>, <div>, whatever fits your design system
  • Styles: no CSS is injected; you apply your own classes
  • Accessibility: role, aria-sort, aria-selected, screen-reader labels
  • Pagination UI: the hooks give you currentPage, rowsPerPage, and handleChangePage; you render the controls
  • Selection UI: tableState.selectedRows, handleSelectedRow, handleSelectAllRows give you the data; you render checkboxes

Staying on the styled <DataTable />

None of these exports affect the default <DataTable />. It continues to work identically. The hooks are an additive unlocked door, not a replacement.


Live demo

The example below is built entirely from the four hooks. No <DataTable /> component. It has sortable headers, per-column filter inputs, manual pagination controls, and striped rows, all in custom markup with no injected CSS.