Docs
CodeRabbit
Cloudflare
AG Grid
SerpAPI
Netlify
Neon
WorkOS
Clerk
Convex
Electric
PowerSync
Sentry
Railway
Prisma
Strapi
Unkey
CodeRabbit
Cloudflare
AG Grid
SerpAPI
Netlify
Neon
WorkOS
Clerk
Convex
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
Legacy API Reference
Enterprise
Getting Started

createTableHook Guide

createTableHook is an advanced API for building reusable, composable table configurations. It lets you define features, row models, and pre-bound components once, then reuse them across multiple tables with minimal boilerplate. It is inspired by TanStack Form's createFormHook.

When to use it: Use createTableHook when you have multiple tables that share the same configuration (features, row models, and reusable components). For a single table, useTable is sufficient.

Examples

  • Composable Tables β€” Two tables (Users and Products) sharing the same createTableHook configuration, with table/cell/header components, sorting, filtering, and pagination.
  • Basic useAppTable β€” Minimal example using createTableHook with no pre-bound components.

Setup

Create a shared table configuration file and call createTableHook with your features, row models, and component registries:

tsx
// hooks/table.ts

import {
  createTableHook,
  tableFeatures,
  columnFilteringFeature,
  rowPaginationFeature,
  rowSortingFeature,
  createFilteredRowModel,
  createPaginatedRowModel,
  createSortedRowModel,
  filterFns,
  sortFns,
} from '@tanstack/react-table'

import { PaginationControls, RowCount, TableToolbar } from '../components/table-components'
import { TextCell, NumberCell, StatusCell, ProgressCell } from '../components/cell-components'
import { SortIndicator, ColumnFilter } from '../components/header-components'

export const {
  createAppColumnHelper,
  useAppTable,
  useTableContext,
  useCellContext,
  useHeaderContext,
} = createTableHook({
  _features: tableFeatures({
    columnFilteringFeature,
    rowPaginationFeature,
    rowSortingFeature,
  }),

  _rowModels: {
    sortedRowModel: createSortedRowModel(sortFns),
    filteredRowModel: createFilteredRowModel(filterFns),
    paginatedRowModel: createPaginatedRowModel(),
  },

  getRowId: (row) => row.id,

  tableComponents: {
    PaginationControls,
    RowCount,
    TableToolbar,
  },

  cellComponents: {
    TextCell,
    NumberCell,
    StatusCell,
    ProgressCell,
  },

  headerComponents: {
    SortIndicator,
    ColumnFilter,
  },
})

What createTableHook Returns

ExportDescription
useAppTableHook for creating tables. Merges default options from the hook with per-table options. No need to pass _features or _rowModelsβ€”they come from the hook.
createAppColumnHelperColumn helper with TFeatures pre-bound. Only requires TData. Use createAppColumnHelper<Person>() instead of createColumnHelper<typeof _features, Person>().
useTableContextAccess the table instance inside tableComponents.
useCellContextAccess the cell instance inside cellComponents.
useHeaderContextAccess the header instance inside headerComponents.

Component Registries

tableComponents

Components that need access to the table instance. They are attached to the table object, so you use them as table.PaginationControls, table.RowCount, etc.

Use useTableContext() inside these components:

tsx
export function PaginationControls() {
  const table = useTableContext()

  return (
    <div className="pagination">
      <button onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>
        Previous
      </button>
      <button onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
        Next
      </button>
    </div>
  )
}

cellComponents

Components that render cell content. They are attached to the cell object in column definitions, so you use them as cell.TextCell, cell.NumberCell, etc.

Use useCellContext() inside these components:

tsx
export function TextCell() {
  const cell = useCellContext<string>()
  return <span>{cell.getValue()}</span>
}

export function NumberCell() {
  const cell = useCellContext<number>()
  return <span>{cell.getValue().toLocaleString()}</span>
}

headerComponents

Components that render header/footer content. They are attached to the header object, so you use them as header.SortIndicator, header.ColumnFilter, etc.

Use useHeaderContext() inside these components:

tsx
export function SortIndicator() {
  const header = useHeaderContext()
  const sorted = header.column.getIsSorted()
  if (!sorted) return null
  return <span>{sorted === 'asc' ? 'πŸ”Ό' : 'πŸ”½'}</span>
}

Using useAppTable

Create tables with useAppTableβ€”_features and _rowModels are inherited from the hook:

tsx
const personColumnHelper = createAppColumnHelper<Person>()

function UsersTable() {
  const [data, setData] = useState(() => makeData(1000))

  const columns = useMemo(
    () =>
      personColumnHelper.columns([
        personColumnHelper.accessor('firstName', {
          header: 'First Name',
          cell: ({ cell }) => <cell.TextCell />,
        }),
        personColumnHelper.accessor('age', {
          header: 'Age',
          cell: ({ cell }) => <cell.NumberCell />,
        }),
        personColumnHelper.accessor('status', {
          header: 'Status',
          cell: ({ cell }) => <cell.StatusCell />,
        }),
      ]),
    [],
  )

  const table = useAppTable({
    columns,
    data,
    debugTable: true,
  })

  return (
    <table.AppTable selector={(state) => ({ pagination: state.pagination, sorting: state.sorting })}>
      {({ sorting }) => (
        <div>
          <table.TableToolbar title="Users" onRefresh={() => setData(makeData(1000))} />
          <table>
            <thead>
              {table.getHeaderGroups().map((headerGroup) => (
                <tr key={headerGroup.id}>
                  {headerGroup.headers.map((h) => (
                    <table.AppHeader header={h} key={h.id}>
                      {(header) => (
                        <th onClick={header.column.getToggleSortingHandler()}>
                          <header.FlexRender />
                          <header.SortIndicator />
                          <header.ColumnFilter />
                        </th>
                      )}
                    </table.AppHeader>
                  ))}
                </tr>
              ))}
            </thead>
            <tbody>
              {table.getRowModel().rows.map((row) => (
                <tr key={row.id}>
                  {row.getAllCells().map((c) => (
                    <table.AppCell cell={c} key={c.id}>
                      {(cell) => <td><cell.FlexRender /></td>}
                    </table.AppCell>
                  ))}
                </tr>
              ))}
            </tbody>
          </table>
          <table.PaginationControls />
          <table.RowCount />
        </div>
      )}
    </table.AppTable>
  )
}

AppTable, AppHeader, AppCell, AppFooter

The table returned by useAppTable includes wrapper components that provide context to your registered components:

  • table.AppTable β€” Wraps the table UI and provides a selector prop for optimized re-renders. Renders its children with the selected state.
  • table.AppHeader β€” Wraps a header and provides the enhanced header context (with header.SortIndicator, header.ColumnFilter, etc.) to its render prop.
  • table.AppCell β€” Wraps a cell and provides the enhanced cell context (with cell.TextCell, cell.FlexRender, etc.) to its render prop.
  • table.AppFooter β€” Same as AppHeader but for footer cells.

Optimized Rendering with selector

Pass a selector to table.AppTable to subscribe only to the state slices you need. This reduces re-renders when other state (e.g., column filters) changes but your component doesn't use it:

tsx
<table.AppTable
  selector={(state) => ({
    pagination: state.pagination,
    sorting: state.sorting,
    columnFilters: state.columnFilters,
  })}
>
  {({ sorting, columnFilters }) => (
    // This only re-renders when pagination, sorting, or columnFilters change
    <div>...</div>
  )}
</table.AppTable>

For v8-style behavior (re-render on any state change), pass (state) => state.

Multiple Table Configurations

You can call createTableHook multiple times for different parts of your app:

tsx
// admin-tables.ts
export const { useAppTable: useAdminTable, createAppColumnHelper: createAdminColumnHelper } =
  createTableHook({
    _features: tableFeatures({ rowSortingFeature, columnFilteringFeature, rowSelectionFeature }),
    _rowModels: { /* ... */ },
    cellComponents: { EditableCell, DeleteButton },
  })

// readonly-tables.ts
export const { useAppTable: useReadonlyTable, createAppColumnHelper: createReadonlyColumnHelper } =
  createTableHook({
    _features: tableFeatures({ rowSortingFeature }),
    _rowModels: { /* ... */ },
    cellComponents: { TextCell, NumberCell },
  })

See Also