Tablecraft
API Reference

State hooks

Granular, standalone state hooks for composing your own useReactTable call instead of using useTable.

For advanced setups where you compose your own useReactTable call directly, each piece of useTable's state is available as a standalone hook. Each returns state plus setters compatible with TanStack Table's state and on*Change props.

import {
  usePaginationState,
  useSortState,
  useFilterState,
  useColumnFilterState,
  useRowSelectionState,
  useColumnVisibilityState,
  useColumnPinningState,
  useColumnOrderState,
  useRowPinningState,
  useColumnSizingState,
  useRowExpansionState,
  useGroupingState,
} from '@marvinackerman/tablecraft'

const pagination = usePaginationState({ pageSize: 25 })
const sorting = useSortState({ defaultSort: [{ id: 'createdAt', desc: true }] })
const globalFilter = useFilterState()
const columnFilters = useColumnFilterState()
const rowSelection = useRowSelectionState()
const columnVisibility = useColumnVisibilityState({ defaultVisibility: { id: false } })
const columnPinning = useColumnPinningState()
const columnOrder = useColumnOrderState()
const rowPinning = useRowPinningState()
const columnSizing = useColumnSizingState()
const rowExpansion = useRowExpansionState({ allowMultiple: false })
const grouping = useGroupingState({ defaultGrouping: ['department'] })

usePaginationState

function usePaginationState(options?: PaginationOptions): {
  state: PaginationState
  onPaginationChange: Dispatch<SetStateAction<PaginationState>>
  setPageIndex: (index: number) => void
  setPageSize: (size: number) => void
  nextPage: () => void
  previousPage: () => void
}

PaginationOptions: { pageSize?: number, pageIndex?: number }

useSortState

function useSortState(options?: SortingOptions): {
  state: SortingState
  onSortingChange: Dispatch<SetStateAction<SortingState>>
  clearSorting: () => void
}

SortingOptions: { defaultSort?: SortingState }

useFilterState

function useFilterState(defaultValue?: string): {
  state: string
  onGlobalFilterChange: Dispatch<SetStateAction<string>>
  clear: () => void
}

Takes a plain string default — not an options object.

useColumnFilterState

function useColumnFilterState(defaultState?: ColumnFiltersState): {
  state: ColumnFiltersState
  onColumnFiltersChange: Dispatch<SetStateAction<ColumnFiltersState>>
  setFilter: (columnId: string, value: unknown) => void
  clearFilter: (columnId: string) => void
  clearAll: () => void
}

Takes a ColumnFiltersState default directly — not an options object.

useRowSelectionState

function useRowSelectionState(options?: UseRowSelectionOptions): {
  state: RowSelectionState
  onRowSelectionChange: Dispatch<SetStateAction<RowSelectionState>>
  toggleRow: (rowId: string) => void
  toggleAll: (rowIds: string[]) => void
  clearSelection: () => void
  selectedRowIds: string[]
  selectedCount: number
  isSelected: (rowId: string) => boolean
  enableMultiRowSelection: boolean
}

UseRowSelectionOptions: { defaultSelection?: RowSelectionState, enableMultiRowSelection?: boolean }

Note: toggleAll here takes the full array of row IDs ((rowIds: string[]) => void) — unlike useTable's rowSelection.toggleAll(), which takes no arguments because it already has the table's rows in scope.

useColumnVisibilityState

function useColumnVisibilityState(options?: UseColumnVisibilityOptions): {
  state: ColumnVisibilityState
  onColumnVisibilityChange: Dispatch<SetStateAction<ColumnVisibilityState>>
  toggleColumn: (columnId: string) => void
  showColumn: (columnId: string) => void
  hideColumn: (columnId: string) => void
  showAll: () => void
  hiddenColumns: string[]
}

UseColumnVisibilityOptions: { defaultVisibility?: ColumnVisibilityState }

useColumnPinningState

function useColumnPinningState(options?: ColumnPinningOptions): {
  state: ColumnPinningState
  setState: Dispatch<SetStateAction<ColumnPinningState>>
  pinStart: (id: string) => void
  pinEnd: (id: string) => void
  unpin: (id: string) => void
  clearPinning: () => void
  isPinned: (id: string) => 'start' | 'end' | false
  startColumns: string[]
  endColumns: string[]
}

ColumnPinningOptions: { defaultPinning?: ColumnPinningState }

useColumnOrderState

function useColumnOrderState(options?: ColumnOrderOptions): {
  state: ColumnOrderState
  setState: Dispatch<SetStateAction<ColumnOrderState>>
  setOrder: (columnIds: string[]) => void
  moveColumn: (columnId: string, toIndex: number) => void
  resetOrder: () => void
  order: string[]
}

ColumnOrderOptions: { defaultOrder?: ColumnOrderState }

An empty order is TanStack's own sentinel for natural column order, so moveColumn has nothing to reorder until you seed defaultOrder — e.g. table.getAllLeafColumns().map(c => c.id). This hook supplies state and a moveColumn helper only; it does not provide a drag interaction. That is deliberate: dnd-kit does drag-and-drop better than tablecraft could, and pulling it in as a dependency would add a heavy new peer for a capability most consumers don't need. Wire moveColumn to whatever drag library you already use, or to plain buttons.

useRowPinningState

function useRowPinningState(options?: RowPinningOptions): {
  state: RowPinningState
  setState: Dispatch<SetStateAction<RowPinningState>>
  pinTop: (rowId: string) => void
  pinBottom: (rowId: string) => void
  unpin: (rowId: string) => void
  clearPinning: () => void
  isPinned: (rowId: string) => 'top' | 'bottom' | false
  topRows: string[]
  bottomRows: string[]
}

RowPinningOptions: { defaultPinning?: RowPinningState }

Rows pin to 'top' / 'bottom', not the 'start' / 'end' vocabulary useColumnPinningState uses. Columns pin along the row axis, where RTL layouts flip "left" and "right" — hence start/end. Rows pin along the vertical axis, which RTL doesn't affect, so TanStack keeps the plainer top/bottom here instead.

useColumnSizingState

function useColumnSizingState(options?: ColumnSizingOptions): {
  state: ColumnSizingState
  setState: Dispatch<SetStateAction<ColumnSizingState>>
  setSize: (columnId: string, px: number) => void
  resetSize: (columnId: string) => void
  resetAll: () => void
  getSize: (columnId: string) => number | undefined
}

ColumnSizingOptions: { defaultSizing?: ColumnSizingState }

This is the committed-width half of resizing — a plain map of column id to pixel width. getSize returns undefined, not 0, for a column with no entry in that map: an unset column isn't zero-width, it falls back to the columnDef's own size, and returning undefined keeps that distinction visible instead of papering over it with a fake 0. The other half — the drag interaction that writes into this state — is useColumnResizing, which needs a table instance and so isn't a standalone options-based hook like the others on this page.

useRowExpansionState

function useRowExpansionState(options?: UseRowExpansionOptions): {
  state: ExpandedState
  onExpandedChange: OnChangeFn<ExpandedState>
  toggleRow: (rowId: string) => void
  expandRow: (rowId: string) => void
  collapseRow: (rowId: string) => void
  clearExpansion: () => void
  expandedRowIds: string[]
  isExpanded: (rowId: string) => boolean
}

UseRowExpansionOptions (granular-hook form): { defaultExpanded?: ExpandedState, allowMultiple?: boolean }. allowMultiple defaults to true; set false so expanding a new row collapses all others.

Note: useTable's rowExpansion option accepts a wider RowExpansionOptions that also includes paginateExpandedRows and getSubRows (for nested sub-rows) — those two extra fields are not part of this standalone hook's options.

useGroupingState

function useGroupingState(options?: UseGroupingOptions): {
  state: GroupingState
  onGroupingChange: OnChangeFn<GroupingState>
  toggleGrouping: (columnId: string) => void
  setGrouping: (cols: GroupingState) => void
  clearGrouping: () => void
  isGrouped: (columnId: string) => boolean
  groupedColumns: GroupingState
}

UseGroupingOptions: { defaultGrouping?: GroupingState, manualGrouping?: boolean, groupedColumnMode?: false | 'reorder' | 'remove' }

On this page