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

Composable Tables Guide

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

Use this pattern when several tables should share behavior and rendering conventions. For one standalone table, createTable 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/solid-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,
  sortedRowModel: createSortedRowModel(),
  filteredRowModel: createFilteredRowModel(),
  paginatedRowModel: createPaginatedRowModel(),
  sortFns,
  filterFns,
})

export const {
  createAppColumnHelper,
  createAppTable,
  useTableContext,
  useCellContext,
  useHeaderContext,
} = createTableHook({
  features,
  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/solid-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,
  sortedRowModel: createSortedRowModel(),
  filteredRowModel: createFilteredRowModel(),
  paginatedRowModel: createPaginatedRowModel(),
  sortFns,
  filterFns,
})

export const {
  createAppColumnHelper,
  createAppTable,
  useTableContext,
  useCellContext,
  useHeaderContext,
} = createTableHook({
  features,
  getRowId: (row) => row.id,
  tableComponents: {
    PaginationControls,
    RowCount,
    TableToolbar,
  },
  cellComponents: {
    TextCell,
    NumberCell,
    StatusCell,
    ProgressCell,
    RowActionsCell,
    PriceCell,
    CategoryCell,
  },
  headerComponents: {
    SortIndicator,
    ColumnFilter,
    FooterColumnId,
    FooterSum,
  },
})

Returned Helpers

HelperPurpose
createAppTableCreates a Solid 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 bound to the app setup, registered JSX components are available on cell and header contexts.

tsx
const personColumnHelper = createAppColumnHelper<Person>()

const columns = 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 = 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 />,
  }),
])

Table Rendering

Create each table with createAppTable. You provide table-specific options like key, columns, and reactive data; the shared table infrastructure comes from the hook.

tsx
const [data, setData] = createSignal(makeData(1_000))

const table = createAppTable({
  key: 'users-table',
  columns,
  get data() {
    return data()
  },
  debugTable: true,
})
const [data, setData] = createSignal(makeData(1_000))

const table = createAppTable({
  key: 'users-table',
  columns,
  get data() {
    return data()
  },
  debugTable: true,
})

The returned table includes JSX wrappers. The example uses table.AppTable with a selector to subscribe to the state slices used by the table UI.

tsx
<table.AppTable
  selector={(state) => ({
    pagination: state.pagination,
    sorting: state.sorting,
    columnFilters: state.columnFilters,
  })}
>
  {(state) => {
    const sorting = () => state().sorting

    return (
      <div class="table-container">
        <table.TableToolbar title="Users Table" onRefresh={refreshData} />

        <table>
          <thead>
            <For each={table.getHeaderGroups()}>
              {(headerGroup) => (
                <tr>
                  <For each={headerGroup.headers}>
                    {(h) => (
                      <table.AppHeader header={h}>
                        {(header) => (
                          <th onClick={header.column.getToggleSortingHandler()}>
                            <header.FlexRender />
                            <header.SortIndicator />
                            <header.ColumnFilter />
                          </th>
                        )}
                      </table.AppHeader>
                    )}
                  </For>
                </tr>
              )}
            </For>
          </thead>
          <tbody>
            <For each={table.getRowModel().rows}>
              {(row) => (
                <tr>
                  <For each={row.getAllCells()}>
                    {(c) => (
                      <table.AppCell cell={c}>
                        {(cell) => (
                          <td>
                            <cell.FlexRender />
                          </td>
                        )}
                      </table.AppCell>
                    )}
                  </For>
                </tr>
              )}
            </For>
          </tbody>
        </table>

        <table.PaginationControls />
        <table.RowCount />
      </div>
    )
  }}
</table.AppTable>
<table.AppTable
  selector={(state) => ({
    pagination: state.pagination,
    sorting: state.sorting,
    columnFilters: state.columnFilters,
  })}
>
  {(state) => {
    const sorting = () => state().sorting

    return (
      <div class="table-container">
        <table.TableToolbar title="Users Table" onRefresh={refreshData} />

        <table>
          <thead>
            <For each={table.getHeaderGroups()}>
              {(headerGroup) => (
                <tr>
                  <For each={headerGroup.headers}>
                    {(h) => (
                      <table.AppHeader header={h}>
                        {(header) => (
                          <th onClick={header.column.getToggleSortingHandler()}>
                            <header.FlexRender />
                            <header.SortIndicator />
                            <header.ColumnFilter />
                          </th>
                        )}
                      </table.AppHeader>
                    )}
                  </For>
                </tr>
              )}
            </For>
          </thead>
          <tbody>
            <For each={table.getRowModel().rows}>
              {(row) => (
                <tr>
                  <For each={row.getAllCells()}>
                    {(c) => (
                      <table.AppCell cell={c}>
                        {(cell) => (
                          <td>
                            <cell.FlexRender />
                          </td>
                        )}
                      </table.AppCell>
                    )}
                  </For>
                </tr>
              )}
            </For>
          </tbody>
        </table>

        <table.PaginationControls />
        <table.RowCount />
      </div>
    )
  }}
</table.AppTable>

Reusing The Hook

The example creates personColumnHelper and productColumnHelper from the same createAppColumnHelper, then creates both Users and Products tables with createAppTable. Each table keeps its own signals and columns, while the shared hook owns features, row models, row IDs, and component conventions.