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

React Example: Composable Tables

tsx
import * as React from 'react'
import { useCallback, useMemo, useState } from 'react'
import ReactDOM from 'react-dom/client'
import { createAppColumnHelper, useAppTable } from './hooks/table'
import { makeData, makeProductData } from './makeData'
import type { Person, Product } from './makeData'
import './index.css'
// Import cell components directly - they use useCellContext internally

// Create column helpers with TFeatures already bound - only need TData!
const personColumnHelper = createAppColumnHelper<Person>()
const productColumnHelper = createAppColumnHelper<Product>()

// Users Table Component - Original implementation
function UsersTable() {
  // Data state
  const [data, setData] = useState(() => makeData(1000))

  // Refresh data callback
  const refreshData = useCallback(() => {
    setData(makeData(1000))
  }, [])

  // Define columns using the column helper
  const columns = useMemo(
    () =>
      // NOTE: You must use `createAppColumnHelper` instead of `createColumnHelper` when using pre-bound components like <cell.TextCell />
      personColumnHelper.columns([
        personColumnHelper.accessor('firstName', {
          header: 'First Name',
          footer: (props) => props.column.id,
          cell: ({ cell }) => <cell.TextCell />,
        }),
        personColumnHelper.accessor('lastName', {
          header: 'Last 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.accessor('visits', {
          header: 'Visits',
          footer: (props) => props.column.id,
          cell: ({ cell }) => <cell.NumberCell />,
        }),
        personColumnHelper.accessor('status', {
          header: 'Status',
          footer: (props) => props.column.id,
          cell: ({ cell }) => <cell.StatusCell />,
        }),
        personColumnHelper.accessor('progress', {
          header: 'Progress',
          footer: (props) => props.column.id,
          cell: ({ cell }) => <cell.ProgressCell />,
        }),
        personColumnHelper.display({
          id: 'actions',
          header: 'Actions',
          cell: ({ cell }) => <cell.RowActionsCell />,
        }),
      ]),
    [],
  )

  // Create the table - _features and _rowModels are already configured!
  const table = useAppTable(
    {
      columns,
      data,
      debugTable: true,
      // more table options
    },
    // (state) => state, // alternatively, subscribe to the entire state instead of using table.Subscribe or selectors down below
  )

  return (
    // Main selector on AppTable - selects all needed state in one place
    <table.AppTable
      selector={(state) => ({
        // subscribe to specific states for re-rendering if you are optimizing for maximum performance
        pagination: state.pagination,
        sorting: state.sorting,
        columnFilters: state.columnFilters,
      })}
    >
      {({ sorting, columnFilters }) => (
        <div className="table-container">
          {/* Table toolbar using pre-bound component */}
          <table.TableToolbar title="Users Table" onRefresh={refreshData} />

          {/* Table element */}
          <table>
            <thead>
              {table.getHeaderGroups().map((headerGroup) => (
                <tr key={headerGroup.id}>
                  {headerGroup.headers.map((h) => (
                    <table.AppHeader header={h} key={h.id}>
                      {(header) => (
                        <th
                          colSpan={header.colSpan}
                          className={
                            header.column.getCanSort() ? 'sortable-header' : ''
                          }
                          onClick={header.column.getToggleSortingHandler()}
                        >
                          {header.isPlaceholder ? null : (
                            <>
                              <header.FlexRender />
                              <header.SortIndicator />
                              <header.ColumnFilter />
                              {/* Show sort order number when multiple columns sorted */}
                              {sorting.length > 1 &&
                                sorting.findIndex(
                                  (s) => s.id === header.column.id,
                                ) > -1 && (
                                  <span className="sort-order">
                                    {sorting.findIndex(
                                      (s) => s.id === header.column.id,
                                    ) + 1}
                                  </span>
                                )}
                            </>
                          )}
                        </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 components are pre-bound via AppCell */}
                          <cell.FlexRender />
                        </td>
                      )}
                    </table.AppCell>
                  ))}
                </tr>
              ))}
            </tbody>
            <tfoot>
              {table.getFooterGroups().map((footerGroup) => (
                <tr key={footerGroup.id}>
                  {footerGroup.headers.map((f) => (
                    <table.AppFooter header={f} key={f.id}>
                      {(footer) => {
                        const columnId = footer.column.id
                        const hasFilter = columnFilters.some(
                          (cf) => cf.id === columnId,
                        )

                        return (
                          <td colSpan={footer.colSpan}>
                            {footer.isPlaceholder ? null : (
                              <>
                                {/* Use FooterSum for numeric columns, FooterColumnId for others */}
                                {columnId === 'age' ||
                                columnId === 'visits' ||
                                columnId === 'progress' ? (
                                  <>
                                    <footer.FooterSum />
                                    {hasFilter && (
                                      <span className="filtered-indicator">
                                        {' '}
                                        (filtered)
                                      </span>
                                    )}
                                  </>
                                ) : columnId === 'actions' ? null : (
                                  <>
                                    <footer.FooterColumnId />
                                    {hasFilter && (
                                      <span className="filtered-indicator">
                                        {' '}
                                        ✓
                                      </span>
                                    )}
                                  </>
                                )}
                              </>
                            )}
                          </td>
                        )
                      }}
                    </table.AppFooter>
                  ))}
                </tr>
              ))}
            </tfoot>
          </table>

          {/* Pagination using pre-bound component */}
          <table.PaginationControls />

          {/* Row count using pre-bound component */}
          <table.RowCount />
        </div>
      )}
    </table.AppTable>
  )
}

// Products Table Component - New implementation using same hook and components
function ProductsTable() {
  // Data state
  const [data, setData] = useState(() => makeProductData(500))

  // Refresh data callback
  const refreshData = useCallback(() => {
    setData(makeProductData(500))
  }, [])

  // Define columns using the column helper - different structure than Users table
  const columns = useMemo(
    () =>
      productColumnHelper.columns([
        productColumnHelper.accessor('name', {
          header: 'Product Name',
          footer: (props) => props.column.id,
          cell: ({ cell }) => <cell.TextCell />,
        }),
        productColumnHelper.accessor('category', {
          header: 'Category',
          footer: (props) => props.column.id,
          cell: ({ cell }) => <cell.CategoryCell />,
        }),
        productColumnHelper.accessor('price', {
          header: 'Price',
          footer: (props) => props.column.id,
          cell: ({ cell }) => <cell.PriceCell />,
        }),
        productColumnHelper.accessor('stock', {
          header: 'In Stock',
          footer: (props) => props.column.id,
          cell: ({ cell }) => <cell.NumberCell />,
        }),
        productColumnHelper.accessor('rating', {
          header: 'Rating',
          footer: (props) => props.column.id,
          cell: ({ cell }) => <cell.ProgressCell />,
        }),
      ]),
    [],
  )

  // Create the table using the same useAppTable hook
  const table = useAppTable({
    columns,
    data,
    getRowId: (row) => row.id,
  })

  return (
    <table.AppTable
      selector={(state) => ({
        pagination: state.pagination,
        sorting: state.sorting,
        columnFilters: state.columnFilters,
      })}
    >
      {({ sorting, columnFilters }) => (
        <div className="table-container">
          {/* Table toolbar using the same pre-bound component */}
          <table.TableToolbar title="Products Table" onRefresh={refreshData} />

          {/* Table element */}
          <table>
            <thead>
              {table.getHeaderGroups().map((headerGroup) => (
                <tr key={headerGroup.id}>
                  {headerGroup.headers.map((h) => (
                    <table.AppHeader header={h} key={h.id}>
                      {(header) => (
                        <th
                          colSpan={header.colSpan}
                          className={
                            header.column.getCanSort() ? 'sortable-header' : ''
                          }
                          onClick={header.column.getToggleSortingHandler()}
                        >
                          {header.isPlaceholder ? null : (
                            <>
                              <header.FlexRender />
                              <header.SortIndicator />
                              <header.ColumnFilter />
                              {sorting.length > 1 &&
                                sorting.findIndex(
                                  (s) => s.id === header.column.id,
                                ) > -1 && (
                                  <span className="sort-order">
                                    {sorting.findIndex(
                                      (s) => s.id === header.column.id,
                                    ) + 1}
                                  </span>
                                )}
                            </>
                          )}
                        </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 components are pre-bound via AppCell */}
                          <cell.FlexRender />
                        </td>
                      )}
                    </table.AppCell>
                  ))}
                </tr>
              ))}
            </tbody>
            <tfoot>
              {table.getFooterGroups().map((footerGroup) => (
                <tr key={footerGroup.id}>
                  {footerGroup.headers.map((f) => (
                    <table.AppFooter header={f} key={f.id}>
                      {(footer) => {
                        const columnId = footer.column.id
                        const hasFilter = columnFilters.some(
                          (cf) => cf.id === columnId,
                        )

                        return (
                          <td colSpan={footer.colSpan}>
                            {footer.isPlaceholder ? null : (
                              <>
                                {/* Use FooterSum for numeric columns, FooterColumnId for others */}
                                {columnId === 'price' ||
                                columnId === 'stock' ||
                                columnId === 'rating' ? (
                                  <>
                                    <footer.FooterSum />
                                    {hasFilter && (
                                      <span className="filtered-indicator">
                                        {' '}
                                        (filtered)
                                      </span>
                                    )}
                                  </>
                                ) : (
                                  <>
                                    <footer.FooterColumnId />
                                    {hasFilter && (
                                      <span className="filtered-indicator">
                                        {' '}
                                        ✓
                                      </span>
                                    )}
                                  </>
                                )}
                              </>
                            )}
                          </td>
                        )
                      }}
                    </table.AppFooter>
                  ))}
                </tr>
              ))}
            </tfoot>
          </table>

          {/* Pagination using the same pre-bound component */}
          <table.PaginationControls />

          {/* Row count using the same pre-bound component */}
          <table.RowCount />
        </div>
      )}
    </table.AppTable>
  )
}

function App() {
  return (
    <div className="app">
      <h1>Composable Tables Example</h1>
      <p className="description">
        Both tables below use the same <code>useAppTable</code> hook and
        shareable components, but with different data types and column
        configurations.
      </p>

      {/* Original Users Table */}
      <UsersTable />

      <div className="table-divider" />

      {/* New Products Table */}
      <ProductsTable />
    </div>
  )
}

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

ReactDOM.createRoot(rootElement).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)