sorting

star 28.1k

Sort rows in TanStack Table v9 with the `sortedRowModel` stage. Covers `rowSortingFeature` + `createSortedRowModel()` (registered on `features`), the built-in `sortFns` registry (renamed from v8 `sortingFns`), `state.sorting` (SortingState = Array<{ id, desc }>), `onSortingChange`, `columnDef.sortFn` (string | function | 'auto'), `sortDescFirst`, `sortUndefined` ('first'|'last'|-1|1|false), `invertSorting`, `enableMultiSort`, `maxMultiSortColCount`, `isMultiSortEvent`, `table.setSorting` / `resetSorting`, `column.getToggleSortingHandler` / `getNextSortingOrder` / `clearSorting` / `getCanSort` / `getCanMultiSort`, `manualSorting` for server-side, and fuzzy `compareItems` pairing.

TanStack By TanStack schedule Updated 6/12/2026

name: sorting description: > Sort rows in TanStack Table v9 with the sortedRowModel stage. Covers rowSortingFeature + createSortedRowModel() (registered on features), the built-in sortFns registry (renamed from v8 sortingFns), state.sorting (SortingState = Array<{ id, desc }>), onSortingChange, columnDef.sortFn (string | function | 'auto'), sortDescFirst, sortUndefined ('first'|'last'|-1|1|false), invertSorting, enableMultiSort, maxMultiSortColCount, isMultiSortEvent, table.setSorting / resetSorting, column.getToggleSortingHandler / getNextSortingOrder / clearSorting / getCanSort / getCanMultiSort, manualSorting for server-side, and fuzzy compareItems pairing. type: core library: tanstack-table library_version: '9.0.0-alpha.48' requires: - state-management - customizing-feature-behavior sources: - TanStack/table:docs/guide/sorting.md - TanStack/table:packages/table-core/src/fns/sortFns.ts - TanStack/table:packages/table-core/src/features/row-sorting/createSortedRowModel.ts - TanStack/table:packages/table-core/src/features/row-sorting/rowSortingFeature.utils.ts - TanStack/table:examples/react/sorting/src/main.tsx

This skill builds on tanstack-table/state-management and tanstack-table/customizing-feature-behavior. Read those first for the atom model and sortFn overrides.

Setup

import {
  tableFeatures,
  rowSortingFeature,
  createSortedRowModel,
  sortFns,
  createColumnHelper,
  constructTable,
} from '@tanstack/table-core'
import type { SortingState } from '@tanstack/table-core'

type Person = {
  firstName: string
  lastName: string
  age: number
  status: 'single' | 'complicated' | 'relationship'
}

const features = tableFeatures({
  rowSortingFeature,
  sortedRowModel: createSortedRowModel(),
  sortFns,
})
const columnHelper = createColumnHelper<typeof features, Person>()

const columns = columnHelper.columns([
  columnHelper.accessor('firstName', { sortFn: 'alphanumeric' }),
  columnHelper.accessor('lastName', {
    sortUndefined: 'last',
    sortDescFirst: false,
  }),
  columnHelper.accessor('age', { sortFn: 'basic' }),
])

const table = constructTable({
  features,
  columns,
  data,
  initialState: { sorting: [] satisfies SortingState },
})

table.setSorting([{ id: 'age', desc: true }])

Core Patterns

Clickable header sorting with multi-sort on Shift+click

// From examples/react/sorting/src/main.tsx
{
  headerGroup.headers.map((header) => (
    <th
      key={header.id}
      onClick={header.column.getToggleSortingHandler()}
      style={{ cursor: header.column.getCanSort() ? 'pointer' : 'default' }}
    >
      <table.FlexRender header={header} />
      {{ asc: ' πŸ”Ό', desc: ' πŸ”½' }[header.column.getIsSorted() as string] ??
        null}
    </th>
  ))
}

getToggleSortingHandler already handles multi-sort when the user holds Shift (configurable via isMultiSortEvent).

Custom sortFn for an enum

// From examples/react/sorting/src/main.tsx
const sortStatusFn: SortFn<typeof features, Person> = (
  rowA,
  rowB,
  _columnId,
) => {
  const statusOrder = ['single', 'complicated', 'relationship']
  return (
    statusOrder.indexOf(rowA.original.status) -
    statusOrder.indexOf(rowB.original.status)
  )
}

columnHelper.accessor('status', { sortFn: sortStatusFn })

Always return an ascending-order comparison. The row model multiplies by -1 for descending and again for invertSorting.

Direction control with sortUndefined and invertSorting

columnHelper.accessor('rank', {
  invertSorting: true, // rank 1 above rank 2 even when "descending"
})

columnHelper.accessor('lastName', {
  sortUndefined: 'last', // ABSOLUTE: end regardless of asc/desc
  sortDescFirst: false,
})

sortUndefined literals ('first', 'last') are absolute. Numeric (-1, 1) flips with desc.

Server-side sorting

const [sorting, setSorting] = useState<SortingState>([])
const { data } = useQuery({
  queryKey: ['rows', sorting],
  queryFn: () =>
    fetch('/api/rows?sort=' + serialize(sorting)).then((r) => r.json()),
})

const table = useTable({
  features: tableFeatures({ rowSortingFeature }),
  // omit sortedRowModel from features β€” server sorts
  columns,
  data,
  manualSorting: true,
  state: { sorting },
  onSortingChange: setSorting,
})

Common Mistakes

[HIGH] Using v8 sortingFn / sortingFns names

Wrong:

{
  accessorKey: 'fullName',
  sortingFn: 'alphanumeric', // v8 name β€” falls through to sortFn_basic
}
// useTable({ sortingFns: { ...sortingFns, myFn } }) // v8 option name

Correct:

import { sortFns, createSortedRowModel } from '@tanstack/react-table'

columnHelper.accessor('firstName', {
  sortFn: 'alphanumeric',
})

const features = tableFeatures({
  rowSortingFeature,
  sortedRowModel: createSortedRowModel(),
  sortFns: {
    ...sortFns,
    myCustom: (a, b, id) => a.original[id] - b.original[id],
  },
})

const table = useTable({ features, columns, data })

v9 renamed columnDef.sortingFn β†’ sortFn, the fn registry slot sortingFns β†’ sortFns (now registered on features), exported registry sortingFns β†’ sortFns. The new column option defaults to 'auto' and falls back to sortFn_basic when lookup misses β€” wrong names sort wrong, silently.

Source: packages/table-core/src/features/row-sorting/rowSortingFeature.utils.ts

[MEDIUM] Expecting sortUndefined: 'first' | 'last' to work in v8

Wrong:

// agent assumes numeric and literal forms are interchangeable
{ accessorKey: 'lastName', sortUndefined: -1 } // ascending-first, descending-LAST

Correct:

// From examples/react/sorting/src/main.tsx
columnHelper.accessor((row) => row.lastName, {
  id: 'lastName',
  sortUndefined: 'last', // ABSOLUTE: always at end regardless of asc/desc
  sortDescFirst: false,
})

v8 only had false | -1 | 1. v9 added 'first' / 'last'. Numeric flips with desc; literals are absolute.

Source: packages/table-core/src/features/row-sorting/createSortedRowModel.ts

[MEDIUM] Custom sortFn factors desc in itself

Wrong:

// takes sort direction into account, breaks the toggle
const customSort: SortFn<any, any> = (a, b, id, desc) => {
  // desc isn't even a parameter β€” agents try to detect via state
  const cmp = a.original[id] - b.original[id]
  return desc ? -cmp : cmp
}

Correct:

// From examples/react/sorting/src/main.tsx
// Always return ascending; the row model handles desc & invertSorting.
const sortStatusFn: SortFn<any, any> = (rowA, rowB, _columnId) => {
  const statusOrder = ['single', 'complicated', 'relationship']
  return (
    statusOrder.indexOf(rowA.original.status) -
    statusOrder.indexOf(rowB.original.status)
  )
}

From the docs guide: "The comparison function does not need to take whether or not the column is in descending or ascending order into account. The row models will take care of that logic." Doubly-flipping yields broken toggles.

Source: packages/table-core/src/features/row-sorting/createSortedRowModel.ts

[MEDIUM] Fuzzy filter without a fuzzy-aware sortFn

Wrong:

columnHelper.accessor('fullName', {
  filterFn: 'fuzzy',
  // BUG: rows sort alphabetically, not by match rank
})

Correct:

import { compareItems } from '@tanstack/match-sorter-utils'

const fuzzySort: SortFn<typeof features, Person> = (rowA, rowB, columnId) => {
  let dir = 0
  if (rowA.columnFiltersMeta[columnId]) {
    dir = compareItems(
      rowA.columnFiltersMeta[columnId].itemRank!,
      rowB.columnFiltersMeta[columnId].itemRank!,
    )
  }
  return dir === 0 ? sortFns.alphanumeric(rowA, rowB, columnId) : dir
}

columnHelper.accessor('fullName', { filterFn: 'fuzzy', sortFn: fuzzySort })

The fuzzy filter writes { itemRank } into row.columnFiltersMeta[columnId] via addMeta. Without a sortFn that reads it, results sort alphabetically and defeat the fuzzy ranking.

Source: examples/react/filters-fuzzy/src/main.tsx

[MEDIUM] getCanSort returns false for display columns under manualSorting

Wrong:

// getCanSort() returns false even though manualSorting is true
const table = useTable({
  manualSorting: true,
  columns: [
    { id: 'computed', header: 'Computed', cell: (info) => row.x + row.y },
  ],
})

Correct:

columnHelper.display({
  id: 'computed',
  header: 'Computed',
  enableSorting: true, // force-enable for manualSorting
  cell: (info) => info.row.original.x + info.row.original.y,
})

getCanSort checks for accessorKey/accessorFn even under manualSorting. Force it on display columns via enableSorting: true (and let the server sort).

Source: https://github.com/TanStack/table/issues/4136

[CRITICAL] Reimplementing what built-in APIs provide

Wrong:

const [sorting, setSorting] = useState([])
const sortedData = useMemo(
  () => [...data].sort(/* …custom… */),
  [data, sorting],
)
// then uses sortedData directly, bypassing the table

Correct:

const features = tableFeatures({
  rowSortingFeature,
  sortedRowModel: createSortedRowModel(),
  sortFns,
})
const table = useTable({ features, columns, data })
// table.setSorting(...), column.toggleSorting(), header.getToggleSortingHandler()

Source: maintainer interview (Phase 4, 2026-05-17)

See also

  • tanstack-table/customizing-feature-behavior β€” sortFn authoring + addMeta chain
  • tanstack-table/filtering β€” fuzzy filter pattern that pairs with fuzzySort
  • tanstack-table/state-management β€” manualSorting + server-side state ownership
Install via CLI
npx skills add https://github.com/TanStack/table --skill sorting
Repository Details
star Stars 28,097
call_split Forks 3,526
navigation Branch main
article Path SKILL.md
More from Creator