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' }