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

Preact Example: Sorting

import { render } from 'preact'
import { useMemo, useReducer, useState } from 'preact/hooks'
import {
  createColumnHelper,
  createSortedRowModel,
  rowSortingFeature,
  sortFns,
  tableFeatures,
  useTable,
} from '@tanstack/preact-table'
import { makeData } from './makeData'
import type { SortFn, SortingState } from '@tanstack/preact-table'
import type { Person } from './makeData'

const features = tableFeatures({
  rowSortingFeature,
  sortedRowModel: createSortedRowModel(),
  sortFns,
})

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 rerender = useReducer(() => ({}), {})[1]

  const columns = useMemo(
    () =>
      columnHelper.columns([
        columnHelper.accessor('firstName', {
          cell: (info) => info.getValue(),
        }),
        columnHelper.accessor((row) => row.lastName, {
          id: 'lastName',
          cell: (info) => info.getValue(),
          header: () => <span>Last Name</span>,
          sortUndefined: 'last',
          sortDescFirst: false,
        }),
        columnHelper.accessor('age', {
          header: () => 'Age',
        }),
        columnHelper.accessor('visits', {
          header: () => <span>Visits</span>,
          sortUndefined: 'last',
        }),
        columnHelper.accessor('status', {
          header: 'Status',
          sortFn: sortStatusFn,
        }),
        columnHelper.accessor('progress', {
          header: 'Profile Progress',
        }),
        columnHelper.accessor('rank', {
          header: 'Rank',
          invertSorting: true,
        }),
        columnHelper.accessor('createdAt', {
          header: 'Created At',
        }),
      ]),
    [],
  )

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

  // optionally, manage sorting state in your own state management (although preact state causes more re-renders here than necessary)
  const [sorting, setSorting] = useState<SortingState>([])

  console.log('sorting', sorting)

  const table = useTable(
    {
      features,
      columns,
      data,
      debugTable: true,
      state: {
        sorting,
      },
      onSortingChange: setSorting,
      // no need to pass pageCount or rowCount with client-side pagination as it is calculated automatically
      // autoResetPageIndex: false, // turn off page index reset when sorting or filtering - default on/true
      // enableMultiSort: false, //Don't allow shift key to sort multiple columns - default on/true
      // enableSorting: false, // - default on/true
      // enableSortingRemoval: false, //Don't allow - default on/true
      // isMultiSortEvent: (e) => true, //Make all clicks multi-sort - default requires `shift` key
      // maxMultiSortColCount: 3, // only allow 3 columns to be sorted at once - default is Infinity
    },
    (state) => state, // default selector
  )

  return (
    <div className="demo-root">
      <div>
        <button onClick={() => refreshData()}>Regenerate Data</button>
        <button onClick={() => stressTest()}>Stress Test (500k 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>
          <button onClick={() => rerender(0)}>Force Rerender</button>
        </div>
        {/* Store mode: full state for debugging */}
        <pre>{JSON.stringify(table.state, null, 2)}</pre>
      </>
    </div>
  )
}

const rootElement = document.getElementById('root')
if (!rootElement) throw new Error('Failed to find the root element')

render(
  <>
    <App />
  </>,
  rootElement,
)
import { render } from 'preact'
import { useMemo, useReducer, useState } from 'preact/hooks'
import {
  createColumnHelper,
  createSortedRowModel,
  rowSortingFeature,
  sortFns,
  tableFeatures,
  useTable,
} from '@tanstack/preact-table'
import { makeData } from './makeData'
import type { SortFn, SortingState } from '@tanstack/preact-table'
import type { Person } from './makeData'

const features = tableFeatures({
  rowSortingFeature,
  sortedRowModel: createSortedRowModel(),
  sortFns,
})

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 rerender = useReducer(() => ({}), {})[1]

  const columns = useMemo(
    () =>
      columnHelper.columns([
        columnHelper.accessor('firstName', {
          cell: (info) => info.getValue(),
        }),
        columnHelper.accessor((row) => row.lastName, {
          id: 'lastName',
          cell: (info) => info.getValue(),
          header: () => <span>Last Name</span>,
          sortUndefined: 'last',
          sortDescFirst: false,
        }),
        columnHelper.accessor('age', {
          header: () => 'Age',
        }),
        columnHelper.accessor('visits', {
          header: () => <span>Visits</span>,
          sortUndefined: 'last',
        }),
        columnHelper.accessor('status', {
          header: 'Status',
          sortFn: sortStatusFn,
        }),
        columnHelper.accessor('progress', {
          header: 'Profile Progress',
        }),
        columnHelper.accessor('rank', {
          header: 'Rank',
          invertSorting: true,
        }),
        columnHelper.accessor('createdAt', {
          header: 'Created At',
        }),
      ]),
    [],
  )

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

  // optionally, manage sorting state in your own state management (although preact state causes more re-renders here than necessary)
  const [sorting, setSorting] = useState<SortingState>([])

  console.log('sorting', sorting)

  const table = useTable(
    {
      features,
      columns,
      data,
      debugTable: true,
      state: {
        sorting,
      },
      onSortingChange: setSorting,
      // no need to pass pageCount or rowCount with client-side pagination as it is calculated automatically
      // autoResetPageIndex: false, // turn off page index reset when sorting or filtering - default on/true
      // enableMultiSort: false, //Don't allow shift key to sort multiple columns - default on/true
      // enableSorting: false, // - default on/true
      // enableSortingRemoval: false, //Don't allow - default on/true
      // isMultiSortEvent: (e) => true, //Make all clicks multi-sort - default requires `shift` key
      // maxMultiSortColCount: 3, // only allow 3 columns to be sorted at once - default is Infinity
    },
    (state) => state, // default selector
  )

  return (
    <div className="demo-root">
      <div>
        <button onClick={() => refreshData()}>Regenerate Data</button>
        <button onClick={() => stressTest()}>Stress Test (500k 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>
          <button onClick={() => rerender(0)}>Force Rerender</button>
        </div>
        {/* Store mode: full state for debugging */}
        <pre>{JSON.stringify(table.state, null, 2)}</pre>
      </>
    </div>
  )
}

const rootElement = document.getElementById('root')
if (!rootElement) throw new Error('Failed to find the root element')

render(
  <>
    <App />
  </>,
  rootElement,
)