Docs
CodeRabbit
Cloudflare
Railway
Netlify
OpenRouter
SerpAPI
AG Grid
WorkOS
Clerk
Prisma
Electric
Unkey
Sentry
CodeRabbit
Cloudflare
Railway
Netlify
OpenRouter
SerpAPI
AG Grid
WorkOS
Clerk
Prisma
Electric
Unkey
Sentry
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
Core Guides

Composable Tables Guide

Composable tables are app-level table factories built with createTableHook. They let React apps define shared features, row models, default options, and reusable components once, then create multiple tables from that shared setup.

Use this pattern when several tables should share the same behavior and component conventions. For one standalone table, useTable is usually enough.

Examples

Setup

The composable tables example keeps the shared configuration in src/hooks/table.ts.

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

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

const features = tableFeatures({
  columnFilteringFeature,
  rowPaginationFeature,
  rowSortingFeature,
})

export const {
  createAppColumnHelper,
  useAppTable,
  useTableContext,
  useCellContext,
  useHeaderContext,
} = createTableHook({
  features,
  rowModels: {
    sortedRowModel: createSortedRowModel(sortFns),
    filteredRowModel: createFilteredRowModel(filterFns),
    paginatedRowModel: createPaginatedRowModel(),
  },
  getRowId: (row) => row.id,
  tableComponents: {
    PaginationControls,
    RowCount,
    TableToolbar,
  },
  cellComponents: {
    TextCell,
    NumberCell,
    StatusCell,
    ProgressCell,
    RowActionsCell,
    PriceCell,
    CategoryCell,
  },
  headerComponents: {
    SortIndicator,
    ColumnFilter,
    FooterColumnId,
    FooterSum,
  },
})
import {
  columnFilteringFeature,
  createFilteredRowModel,
  createPaginatedRowModel,
  createSortedRowModel,
  createTableHook,
  filterFns,
  rowPaginationFeature,
  rowSortingFeature,
  sortFns,
  tableFeatures,
} from '@tanstack/react-table'

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

const features = tableFeatures({
  columnFilteringFeature,
  rowPaginationFeature,
  rowSortingFeature,
})

export const {
  createAppColumnHelper,
  useAppTable,
  useTableContext,
  useCellContext,
  useHeaderContext,
} = createTableHook({
  features,
  rowModels: {
    sortedRowModel: createSortedRowModel(sortFns),
    filteredRowModel: createFilteredRowModel(filterFns),
    paginatedRowModel: createPaginatedRowModel(),
  },
  getRowId: (row) => row.id,
  tableComponents: {
    PaginationControls,
    RowCount,
    TableToolbar,
  },
  cellComponents: {
    TextCell,
    NumberCell,
    StatusCell,
    ProgressCell,
    RowActionsCell,
    PriceCell,
    CategoryCell,
  },
  headerComponents: {
    SortIndicator,
    ColumnFilter,
    FooterColumnId,
    FooterSum,
  },
})

Returned Helpers

HelperPurpose
useAppTableCreates a table with shared features, row models, defaults, and registered components.
createAppColumnHelperCreates column helpers with TFeatures and registered component types already bound.
useTableContextReads the current table inside registered table components.
useCellContextReads the current cell inside registered cell components.
useHeaderContextReads the current header/footer inside registered header components.

Columns

Create one column helper per row type. Since the helper is already bound to the app table setup, column definitions can reference registered cell and header components directly.

tsx
const personColumnHelper = createAppColumnHelper<Person>()

const columns = useMemo(
  () =>
    personColumnHelper.columns([
      personColumnHelper.accessor('firstName', {
        header: 'First Name',
        footer: (props) => props.column.id,
        cell: ({ cell }) => <cell.TextCell />,
      }),
      personColumnHelper.accessor('age', {
        header: 'Age',
        footer: (props) => props.column.id,
        cell: ({ cell }) => <cell.NumberCell />,
      }),
      personColumnHelper.display({
        id: 'actions',
        header: 'Actions',
        cell: ({ cell }) => <cell.RowActionsCell />,
      }),
    ]),
  [],
)
const personColumnHelper = createAppColumnHelper<Person>()

const columns = useMemo(
  () =>
    personColumnHelper.columns([
      personColumnHelper.accessor('firstName', {
        header: 'First Name',
        footer: (props) => props.column.id,
        cell: ({ cell }) => <cell.TextCell />,
      }),
      personColumnHelper.accessor('age', {
        header: 'Age',
        footer: (props) => props.column.id,
        cell: ({ cell }) => <cell.NumberCell />,
      }),
      personColumnHelper.display({
        id: 'actions',
        header: 'Actions',
        cell: ({ cell }) => <cell.RowActionsCell />,
      }),
    ]),
  [],
)

Registered cell components use useCellContext() internally, and registered header/footer components use useHeaderContext().

Table Rendering

Create each table with useAppTable. You pass table-specific options like key, columns, and data; the shared features, rowModels, getRowId, and component registries come from the hook.

tsx
const table = useAppTable(
  {
    key: 'users-table',
    columns,
    data,
    debugTable: true,
  },
  (state) => state,
)
const table = useAppTable(
  {
    key: 'users-table',
    columns,
    data,
    debugTable: true,
  },
  (state) => state,
)

The returned table includes AppTable, AppHeader, AppCell, and AppFooter wrappers. The example uses AppTable with a selector so rendering can subscribe to the state slices used by that table.

tsx
<table.AppTable
  selector={(state) => ({
    pagination: state.pagination,
    sorting: state.sorting,
    columnFilters: state.columnFilters,
  })}
>
  {({ sorting, columnFilters }) => (
    <div className="table-container">
      <table.TableToolbar title="Users Table" onRefresh={refreshData} />

      <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>
<table.AppTable
  selector={(state) => ({
    pagination: state.pagination,
    sorting: state.sorting,
    columnFilters: state.columnFilters,
  })}
>
  {({ sorting, columnFilters }) => (
    <div className="table-container">
      <table.TableToolbar title="Users Table" onRefresh={refreshData} />

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

Reusing The Hook

The example creates both personColumnHelper and productColumnHelper from the same createAppColumnHelper, then renders Users and Products tables with the same useAppTable factory. Each table owns its data and columns, while the app hook owns table infrastructure and component conventions.