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.
Create a shared table configuration file and call createTableHook with your features, row models, and component registries:
// 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,
},
})
| Export | Description |
|---|---|
| useAppTable | Hook 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. |
| createAppColumnHelper | Column helper with TFeatures pre-bound. Only requires TData. Use createAppColumnHelper<Person>() instead of createColumnHelper<typeof _features, Person>(). |
| useTableContext | Access the table instance inside tableComponents. |
| useCellContext | Access the cell instance inside cellComponents. |
| useHeaderContext | Access the header instance inside headerComponents. |
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:
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>
)
}
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:
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>
}
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:
export function SortIndicator() {
const header = useHeaderContext()
const sorted = header.column.getIsSorted()
if (!sorted) return null
return <span>{sorted === 'asc' ? 'πΌ' : 'π½'}</span>
}
Create tables with useAppTableβ_features and _rowModels are inherited from the hook:
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>
)
}
The table returned by useAppTable includes wrapper components that provide context to your registered components:
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:
<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.
You can call createTableHook multiple times for different parts of your app:
// 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 },
})