Docs
CodeRabbit
Cloudflare
AG Grid
SerpAPI
Netlify
OpenRouter
Neon
WorkOS
Clerk
Electric
PowerSync
Sentry
Railway
Prisma
Strapi
Unkey
CodeRabbit
Cloudflare
AG Grid
SerpAPI
Netlify
OpenRouter
Neon
WorkOS
Clerk
Electric
PowerSync
Sentry
Railway
Prisma
Strapi
Unkey
Table API Reference
Column API Reference
Row API Reference
Cell API Reference
Header API Reference
Features API Reference
Static Functions API Reference
Legacy API Reference

React Example: Sorting

import React from 'react'
import ReactDOM from 'react-dom/client'
import './index.css'
import {
  createColumnHelper,
  createSortedRowModel,
  rowSortingFeature,
  sortFn_alphanumeric,
  sortFn_datetime,
  sortFn_text,
  tableFeatures,
  useTable,
} from '@tanstack/react-table'
import { makeData } from './makeData'
import type { SortFn } from '@tanstack/react-table'
import type { Person } from './makeData'

const features = tableFeatures({
  rowSortingFeature,
  sortedRowModel: createSortedRowModel(),
  sortFns: {
    alphanumeric: sortFn_alphanumeric,
    datetime: sortFn_datetime,
    text: sortFn_text,
  },
})

const columnHelper = createColumnHelper<typeof features, Person>()

// custom sorting logic for one of our enum columns
const sortStatusFn: SortFn<typeof features, Person> = (
  rowA,
  rowB,
  _columnId,
) => {
  const statusA = rowA.original.status
  const statusB = rowB.original.status
  const statusOrder = ['single', 'complicated', 'relationship']
  return statusOrder.indexOf(statusA) - statusOrder.indexOf(statusB)
}

function App() {
  const columns = React.useMemo(
    () =>
      columnHelper.columns([
        columnHelper.display({
          id: 'rowNumber',
          header: '#',
          cell: ({ row }) => row.getDisplayIndex() + 1,
        }),
        columnHelper.accessor('firstName', {
          cell: (info) => info.getValue(),
          // this column will sort in ascending order by default since it is a string column
        }),
        columnHelper.accessor((row) => row.lastName, {
          id: 'lastName',
          cell: (info) => info.getValue(),
          header: () => <span>Last Name</span>,
          sortUndefined: 'last', // force undefined values to the end
          sortDescFirst: false, // first sort order will be ascending (nullable values can mess up auto detection of sort order)
        }),
        columnHelper.accessor('email', {
          header: 'Email',
          sortFn: 'alphanumeric',
        }),
        columnHelper.accessor('age', {
          header: () => 'Age',
          // this column will sort in descending order by default since it is a number column
        }),
        columnHelper.accessor('visits', {
          header: () => <span>Visits</span>,
          sortUndefined: 'last', // force undefined values to the end
        }),
        columnHelper.accessor('status', {
          header: 'Status',
          sortFn: sortStatusFn, // use our custom sorting function for this enum column
        }),
        columnHelper.accessor('progress', {
          header: 'Profile Progress',
          // enableSorting: false, //disable sorting for this column
        }),
        columnHelper.accessor('rank', {
          header: 'Rank',
          invertSorting: true, // invert the sorting order (golf score-like where smaller is better)
        }),
        columnHelper.accessor('createdAt', {
          header: 'Created At',
          // sortFn: 'datetime' //make sure table knows this is a datetime column (usually can detect if no null values)
        }),
      ]),
    [],
  )

  const [data, setData] = React.useState(() => makeData(1_000))
  const refreshData = () => setData(makeData(1_000))
  const stressTest = () => setData(makeData(1_000_000))

  const table = useTable(
    {
      features,
      columns,
      data,
      // initialState: { sorting: [{ id: 'firstName', desc: false }] }, // set the initial sort once
      // atoms: { sorting: sortingAtom }, // preferred: own sorting state with an external atom
      // state: { sorting }, // classic controlled state; pair with onSortingChange
      // onSortingChange: setSorting,
      // enableSorting: false, // disable sorting for every column; default true
      // sortDescFirst: true, // start every sort cycle with descending order; inferred by column data by default
      // enableSortingRemoval: false, // keep a sorted column sorted when toggling; default true
      // enableMultiSort: false, // disable Shift-click multi-sorting; default true
      // enableMultiRemove: false, // prevent a multi-sort toggle from removing a sorted column; default true
      // isMultiSortEvent: () => true, // make every sort interaction a multi-sort; default requires Shift
      // maxMultiSortColCount: 3, // limit multi-sorting to three columns; default Infinity
      // manualSorting: true, // pass data that is already sorted, for example from a server
      // autoResetPageIndex: false, // with pagination, keep the current page when sorting changes; default true
      debugTable: true,
    },
    (state) => state, // default selector
  )

  return (
    <div className="demo-root">
      <div>
        <button onClick={() => refreshData()}>Regenerate Data</button>
        <button onClick={() => stressTest()}>Stress Test (1M rows)</button>
      </div>
      <div className="spacer-sm" />
      <table>
        <thead>
          {table.getHeaderGroups().map((headerGroup) => (
            <tr key={headerGroup.id}>
              {headerGroup.headers.map((header) => {
                return (
                  <th key={header.id} colSpan={header.colSpan}>
                    {header.isPlaceholder ? null : (
                      <div
                        className={
                          header.column.getCanSort() ? 'sortable-header' : ''
                        }
                        onClick={header.column.getToggleSortingHandler()}
                        title={
                          header.column.getCanSort()
                            ? header.column.getNextSortingOrder() === 'asc'
                              ? 'Sort ascending'
                              : header.column.getNextSortingOrder() === 'desc'
                                ? 'Sort descending'
                                : 'Clear sort'
                            : undefined
                        }
                      >
                        <table.FlexRender header={header} />
                        {{
                          asc: ' 🔼',
                          desc: ' 🔽',
                        }[header.column.getIsSorted() as string] ?? null}
                      </div>
                    )}
                  </th>
                )
              })}
            </tr>
          ))}
        </thead>
        <tbody>
          {table
            .getRowModel()
            .rows.slice(0, 10)
            .map((row) => {
              return (
                <tr key={row.id}>
                  {row.getAllCells().map((cell) => {
                    return (
                      <td key={cell.id}>
                        <table.FlexRender cell={cell} />
                      </td>
                    )
                  })}
                </tr>
              )
            })}
        </tbody>
      </table>
      <div>{table.getRowModel().rows.length.toLocaleString()} Rows</div>
      <div></div>
      {/* Store mode: dump full table state for debugging */}
      <pre data-testid="table-state">
        {JSON.stringify(table.state, null, 2)}
      </pre>
    </div>
  )
}

const rootElement = document.getElementById('root')

if (!rootElement) throw new Error('Failed to find the root element')

ReactDOM.createRoot(rootElement).render(
  // <React.StrictMode>
  <App />,
  // </React.StrictMode>,
)
import React from 'react'
import ReactDOM from 'react-dom/client'
import './index.css'
import {
  createColumnHelper,
  createSortedRowModel,
  rowSortingFeature,
  sortFn_alphanumeric,
  sortFn_datetime,
  sortFn_text,
  tableFeatures,
  useTable,
} from '@tanstack/react-table'
import { makeData } from './makeData'
import type { SortFn } from '@tanstack/react-table'
import type { Person } from './makeData'

const features = tableFeatures({
  rowSortingFeature,
  sortedRowModel: createSortedRowModel(),
  sortFns: {
    alphanumeric: sortFn_alphanumeric,
    datetime: sortFn_datetime,
    text: sortFn_text,
  },
})

const columnHelper = createColumnHelper<typeof features, Person>()

// custom sorting logic for one of our enum columns
const sortStatusFn: SortFn<typeof features, Person> = (
  rowA,
  rowB,
  _columnId,
) => {
  const statusA = rowA.original.status
  const statusB = rowB.original.status
  const statusOrder = ['single', 'complicated', 'relationship']
  return statusOrder.indexOf(statusA) - statusOrder.indexOf(statusB)
}

function App() {
  const columns = React.useMemo(
    () =>
      columnHelper.columns([
        columnHelper.display({
          id: 'rowNumber',
          header: '#',
          cell: ({ row }) => row.getDisplayIndex() + 1,
        }),
        columnHelper.accessor('firstName', {
          cell: (info) => info.getValue(),
          // this column will sort in ascending order by default since it is a string column
        }),
        columnHelper.accessor((row) => row.lastName, {
          id: 'lastName',
          cell: (info) => info.getValue(),
          header: () => <span>Last Name</span>,
          sortUndefined: 'last', // force undefined values to the end
          sortDescFirst: false, // first sort order will be ascending (nullable values can mess up auto detection of sort order)
        }),
        columnHelper.accessor('email', {
          header: 'Email',
          sortFn: 'alphanumeric',
        }),
        columnHelper.accessor('age', {
          header: () => 'Age',
          // this column will sort in descending order by default since it is a number column
        }),
        columnHelper.accessor('visits', {
          header: () => <span>Visits</span>,
          sortUndefined: 'last', // force undefined values to the end
        }),
        columnHelper.accessor('status', {
          header: 'Status',
          sortFn: sortStatusFn, // use our custom sorting function for this enum column
        }),
        columnHelper.accessor('progress', {
          header: 'Profile Progress',
          // enableSorting: false, //disable sorting for this column
        }),
        columnHelper.accessor('rank', {
          header: 'Rank',
          invertSorting: true, // invert the sorting order (golf score-like where smaller is better)
        }),
        columnHelper.accessor('createdAt', {
          header: 'Created At',
          // sortFn: 'datetime' //make sure table knows this is a datetime column (usually can detect if no null values)
        }),
      ]),
    [],
  )

  const [data, setData] = React.useState(() => makeData(1_000))
  const refreshData = () => setData(makeData(1_000))
  const stressTest = () => setData(makeData(1_000_000))

  const table = useTable(
    {
      features,
      columns,
      data,
      // initialState: { sorting: [{ id: 'firstName', desc: false }] }, // set the initial sort once
      // atoms: { sorting: sortingAtom }, // preferred: own sorting state with an external atom
      // state: { sorting }, // classic controlled state; pair with onSortingChange
      // onSortingChange: setSorting,
      // enableSorting: false, // disable sorting for every column; default true
      // sortDescFirst: true, // start every sort cycle with descending order; inferred by column data by default
      // enableSortingRemoval: false, // keep a sorted column sorted when toggling; default true
      // enableMultiSort: false, // disable Shift-click multi-sorting; default true
      // enableMultiRemove: false, // prevent a multi-sort toggle from removing a sorted column; default true
      // isMultiSortEvent: () => true, // make every sort interaction a multi-sort; default requires Shift
      // maxMultiSortColCount: 3, // limit multi-sorting to three columns; default Infinity
      // manualSorting: true, // pass data that is already sorted, for example from a server
      // autoResetPageIndex: false, // with pagination, keep the current page when sorting changes; default true
      debugTable: true,
    },
    (state) => state, // default selector
  )

  return (
    <div className="demo-root">
      <div>
        <button onClick={() => refreshData()}>Regenerate Data</button>
        <button onClick={() => stressTest()}>Stress Test (1M rows)</button>
      </div>
      <div className="spacer-sm" />
      <table>
        <thead>
          {table.getHeaderGroups().map((headerGroup) => (
            <tr key={headerGroup.id}>
              {headerGroup.headers.map((header) => {
                return (
                  <th key={header.id} colSpan={header.colSpan}>
                    {header.isPlaceholder ? null : (
                      <div
                        className={
                          header.column.getCanSort() ? 'sortable-header' : ''
                        }
                        onClick={header.column.getToggleSortingHandler()}
                        title={
                          header.column.getCanSort()
                            ? header.column.getNextSortingOrder() === 'asc'
                              ? 'Sort ascending'
                              : header.column.getNextSortingOrder() === 'desc'
                                ? 'Sort descending'
                                : 'Clear sort'
                            : undefined
                        }
                      >
                        <table.FlexRender header={header} />
                        {{
                          asc: ' 🔼',
                          desc: ' 🔽',
                        }[header.column.getIsSorted() as string] ?? null}
                      </div>
                    )}
                  </th>
                )
              })}
            </tr>
          ))}
        </thead>
        <tbody>
          {table
            .getRowModel()
            .rows.slice(0, 10)
            .map((row) => {
              return (
                <tr key={row.id}>
                  {row.getAllCells().map((cell) => {
                    return (
                      <td key={cell.id}>
                        <table.FlexRender cell={cell} />
                      </td>
                    )
                  })}
                </tr>
              )
            })}
        </tbody>
      </table>
      <div>{table.getRowModel().rows.length.toLocaleString()} Rows</div>
      <div></div>
      {/* Store mode: dump full table state for debugging */}
      <pre data-testid="table-state">
        {JSON.stringify(table.state, null, 2)}
      </pre>
    </div>
  )
}

const rootElement = document.getElementById('root')

if (!rootElement) throw new Error('Failed to find the root element')

ReactDOM.createRoot(rootElement).render(
  // <React.StrictMode>
  <App />,
  // </React.StrictMode>,
)