Docs
CodeRabbit
Cloudflare
AG Grid
SerpAPI
Netlify
OpenRouter
WorkOS
Clerk
Electric
PowerSync
Sentry
Railway
Prisma
Strapi
Unkey
CodeRabbit
Cloudflare
AG Grid
SerpAPI
Netlify
OpenRouter
WorkOS
Clerk
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: Row Selection

import React from 'react'
import ReactDOM from 'react-dom/client'
import {
  columnFilteringFeature,
  createColumnHelper,
  createFilteredRowModel,
  createPaginatedRowModel,
  filterFns,
  globalFilteringFeature,
  rowPaginationFeature,
  rowSelectionFeature,
  tableFeatures,
  useTable,
} from '@tanstack/react-table'
import {
  tableDevtoolsPlugin,
  useTanStackTableDevtools,
} from '@tanstack/react-table-devtools'
import { TanStackDevtools } from '@tanstack/react-devtools'
import { useCreateAtom } from '@tanstack/react-store'
import { makeData } from './makeData'
import type { HTMLProps } from 'react'
import type { Person } from './makeData'
import type { Column, RowSelectionState, Table } from '@tanstack/react-table'
import './index.css'

const _features = tableFeatures({
  rowPaginationFeature,
  rowSelectionFeature,
  columnFilteringFeature,
  globalFilteringFeature,
})

const columnHelper = createColumnHelper<typeof _features, Person>()

function App() {
  const rerender = React.useReducer(() => ({}), {})[1]

  const columns = React.useMemo(
    () =>
      columnHelper.columns([
        columnHelper.display({
          id: 'select',
          header: () => {
            return (
              <table.Subscribe
                source={table.atoms.rowSelection} // slice atom only — not the full derived store
                // omit selector to subscribe to the whole rowSelection slice
              >
                {() => (
                  <IndeterminateCheckbox
                    checked={table.getIsAllRowsSelected()}
                    indeterminate={table.getIsSomeRowsSelected()}
                    onChange={table.getToggleAllRowsSelectedHandler()}
                  />
                )}
              </table.Subscribe>
            )
          },
          cell: ({ row }) => (
            <div className="column-toggle-row">
              <IndeterminateCheckbox
                checked={row.getIsSelected()}
                disabled={!row.getCanSelect()}
                indeterminate={row.getIsSomeSelected()}
                onChange={row.getToggleSelectedHandler()}
              />
            </div>
          ),
        }),
        columnHelper.accessor('firstName', {
          header: 'First Name',
          cell: (info) => info.getValue(),
          footer: (props) => props.column.id,
        }),
        columnHelper.accessor((row) => row.lastName, {
          id: 'lastName',
          header: () => <span>Last Name</span>,
          cell: (info) => info.getValue(),
          footer: (props) => props.column.id,
        }),
        columnHelper.accessor('age', {
          header: () => 'Age',
          footer: (props) => props.column.id,
        }),
        columnHelper.accessor('visits', {
          header: () => <span>Visits</span>,
          footer: (props) => props.column.id,
        }),
        columnHelper.accessor('status', {
          header: 'Status',
          footer: (props) => props.column.id,
        }),
        columnHelper.accessor('progress', {
          header: 'Profile Progress',
          footer: (props) => props.column.id,
        }),
      ]),
    [],
  )

  const [data, setData] = React.useState(() => makeData(1_000))
  const refreshData = () => setData(makeData(1_000))
  const stressTest = () => setData(makeData(100_000))

  const rowSelectionAtom = useCreateAtom<RowSelectionState>({})

  const table = useTable(
    {
      _features,
      _rowModels: {
        filteredRowModel: createFilteredRowModel(filterFns),
        paginatedRowModel: createPaginatedRowModel(),
      },
      atoms: {
        rowSelection: rowSelectionAtom,
      },
      columns,
      data,
      getRowId: (row) => row.id,
      enableRowSelection: true, // enable row selection for all rows
      // enableRowSelection: row => row.original.age > 18, // or enable row selection conditionally per row
      debugTable: true,
    },
    // (state) => state, // uncomment to subscribe to the entire table state (this is how v8 used to work by default)
  )

  useTanStackTableDevtools(table, 'Row Selection Example')

  return (
    <>
      <table.Subscribe
        selector={(state) => ({
          // Store mode: multiple slices — must use table.store + explicit selector
          columnFilters: state.columnFilters,
          globalFilter: state.globalFilter,
          pagination: state.pagination,
        })}
      >
        {(state) => (
          <div className="demo-root">
            <div>
              <button
                className="demo-button demo-button-spaced"
                onClick={() => refreshData()}
              >
                Regenerate Data
              </button>
              <button
                className="demo-button demo-button-spaced"
                onClick={() => stressTest()}
              >
                Stress Test (100k rows)
              </button>
            </div>
            <div>
              <DebouncedInput
                value={state.globalFilter ?? ''}
                onChange={(value) => table.setGlobalFilter(value)}
                className="summary-panel"
                placeholder="Search all columns..."
              />
            </div>
            <div className="spacer-sm" />
            <table>
              <thead>
                {table.getHeaderGroups().map((headerGroup) => (
                  <tr key={headerGroup.id}>
                    {headerGroup.headers.map((header) => {
                      return (
                        <th key={header.id} colSpan={header.colSpan}>
                          {header.isPlaceholder ? null : (
                            <>
                              <table.FlexRender header={header} />
                              {header.column.getCanFilter() ? (
                                <div>
                                  <Filter
                                    column={header.column}
                                    table={table}
                                  />
                                </div>
                              ) : null}
                            </>
                          )}
                        </th>
                      )
                    })}
                  </tr>
                ))}
              </thead>
              <tbody>
                {table.getRowModel().rows.map((row) => {
                  return (
                    <table.Subscribe
                      key={row.id}
                      source={table.atoms.rowSelection} // same slice as table.atoms.rowSelection; external atom for clarity
                      selector={(rowSelection) => rowSelection?.[row.id]} // optional: narrow to this row so the row re-renders only when this id toggles
                    >
                      {(_isRowSelected) => (
                        <tr key={row.id}>
                          {row.getAllCells().map((cell) => {
                            return (
                              <td key={cell.id}>
                                <table.FlexRender cell={cell} />
                              </td>
                            )
                          })}
                        </tr>
                      )}
                    </table.Subscribe>
                  )
                })}
              </tbody>
              <tfoot>
                <tr>
                  <td className="cell-padding">
                    <table.Subscribe
                      source={table.atoms.rowSelection} // whole slice; footer toggles don’t need per-row projection
                      selector={(a) => a}
                    >
                      {() => (
                        <IndeterminateCheckbox
                          checked={table.getIsAllPageRowsSelected()}
                          indeterminate={table.getIsSomePageRowsSelected()}
                          onChange={table.getToggleAllPageRowsSelectedHandler()}
                        />
                      )}
                    </table.Subscribe>
                  </td>
                  <td colSpan={20}>
                    Page Rows (
                    {table.getRowModel().rows.length.toLocaleString()})
                  </td>
                </tr>
              </tfoot>
            </table>
            <div className="spacer-sm" />
            <div className="controls">
              <button
                className="demo-button demo-button-sm"
                onClick={() => table.setPageIndex(0)}
                disabled={!table.getCanPreviousPage()}
              >
                {'<<'}
              </button>
              <button
                className="demo-button demo-button-sm"
                onClick={() => table.previousPage()}
                disabled={!table.getCanPreviousPage()}
              >
                {'<'}
              </button>
              <button
                className="demo-button demo-button-sm"
                onClick={() => table.nextPage()}
                disabled={!table.getCanNextPage()}
              >
                {'>'}
              </button>
              <button
                className="demo-button demo-button-sm"
                onClick={() => table.setPageIndex(table.getPageCount() - 1)}
                disabled={!table.getCanNextPage()}
              >
                {'>>'}
              </button>
              <span className="inline-controls">
                <div>Page</div>
                <strong>
                  {(
                    table.store.state.pagination.pageIndex + 1
                  ).toLocaleString()}{' '}
                  of {table.getPageCount().toLocaleString()}
                </strong>
              </span>
              <span className="inline-controls">
                | Go to page:
                <input
                  type="number"
                  min="1"
                  max={table.getPageCount()}
                  defaultValue={table.store.state.pagination.pageIndex + 1}
                  onChange={(e) => {
                    const page = e.target.value ? Number(e.target.value) - 1 : 0
                    table.setPageIndex(page)
                  }}
                  className="page-size-input"
                />
              </span>
              <select
                value={table.store.state.pagination.pageSize}
                onChange={(e) => {
                  table.setPageSize(Number(e.target.value))
                }}
              >
                {[10, 20, 30, 40, 50].map((pageSize) => (
                  <option key={pageSize} value={pageSize}>
                    Show {pageSize}
                  </option>
                ))}
              </select>
            </div>
            <br />
            <div>
              <table.Subscribe
                selector={(state) => ({
                  numSelected: Object.keys(state.rowSelection).length,
                })}
              >
                {({ numSelected }) => <>{numSelected.toLocaleString()} of </>}
              </table.Subscribe>
              {table.getPreFilteredRowModel().rows.length.toLocaleString()}{' '}
              Total Rows Selected
            </div>
            <hr />
            <br />
            <div>
              <button
                className="demo-button demo-button-spaced"
                onClick={() => rerender()}
              >
                Force Rerender
              </button>
            </div>
            <div>
              <button
                className="demo-button demo-button-spaced"
                onClick={() =>
                  console.info(
                    'table.getSelectedRowModel().flatRows',
                    table.getSelectedRowModel().flatRows,
                  )
                }
              >
                Log table.getSelectedRowModel().flatRows
              </button>
            </div>
            <div>
              <label>Row Selection State:</label>
              <table.Subscribe selector={(state) => state}>
                {(state) => <pre>{JSON.stringify(state, null, 2)}</pre>}
              </table.Subscribe>
            </div>
          </div>
        )}
      </table.Subscribe>
    </>
  )
}

function Filter({
  column,
  table,
}: {
  column: Column<typeof _features, Person>
  table: Table<typeof _features, Person>
}) {
  const firstValue = table
    .getPreFilteredRowModel()
    .flatRows[0]?.getValue(column.id)

  return typeof firstValue === 'number' ? (
    <div className="filter-row">
      <DebouncedInput
        type="number"
        value={((column.getFilterValue() as any)?.[0] ?? '') as string}
        onChange={(value) =>
          column.setFilterValue((old: any) => [value, old?.[1]])
        }
        placeholder={`Min`}
        className="filter-input"
      />
      <DebouncedInput
        type="number"
        value={((column.getFilterValue() as any)?.[1] ?? '') as string}
        onChange={(value) =>
          column.setFilterValue((old: any) => [old?.[0], value])
        }
        placeholder={`Max`}
        className="filter-input"
      />
    </div>
  ) : (
    <DebouncedInput
      type="text"
      value={(column.getFilterValue() ?? '') as string}
      onChange={(value) => column.setFilterValue(value)}
      placeholder={`Search...`}
      className="filter-select"
    />
  )
}

// A debounced input react component
function DebouncedInput({
  value: initialValue,
  onChange,
  debounce = 500,
  ...props
}: {
  value: string | number
  onChange: (value: string | number) => void
  debounce?: number
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'>) {
  const [value, setValue] = React.useState(initialValue)

  React.useEffect(() => {
    setValue(initialValue)
  }, [initialValue])

  React.useEffect(() => {
    const timeout = setTimeout(() => {
      onChange(value)
    }, debounce)

    return () => clearTimeout(timeout)
  }, [value])

  return (
    <input
      {...props}
      value={value}
      onChange={(e) => setValue(e.target.value)}
    />
  )
}

function IndeterminateCheckbox({
  indeterminate,
  className = '',
  ...rest
}: { indeterminate?: boolean } & HTMLProps<HTMLInputElement>) {
  const ref = React.useRef<HTMLInputElement>(null!)

  React.useEffect(() => {
    if (typeof indeterminate === 'boolean') {
      ref.current.indeterminate = !rest.checked && indeterminate
    }
  }, [ref, indeterminate])

  return (
    <input
      type="checkbox"
      ref={ref}
      className={className + ' sortable-header'}
      {...rest}
    />
  )
}

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

ReactDOM.createRoot(rootElement).render(
  <React.StrictMode>
    <App />
    <TanStackDevtools plugins={[tableDevtoolsPlugin()]} />
  </React.StrictMode>,
)
import React from 'react'
import ReactDOM from 'react-dom/client'
import {
  columnFilteringFeature,
  createColumnHelper,
  createFilteredRowModel,
  createPaginatedRowModel,
  filterFns,
  globalFilteringFeature,
  rowPaginationFeature,
  rowSelectionFeature,
  tableFeatures,
  useTable,
} from '@tanstack/react-table'
import {
  tableDevtoolsPlugin,
  useTanStackTableDevtools,
} from '@tanstack/react-table-devtools'
import { TanStackDevtools } from '@tanstack/react-devtools'
import { useCreateAtom } from '@tanstack/react-store'
import { makeData } from './makeData'
import type { HTMLProps } from 'react'
import type { Person } from './makeData'
import type { Column, RowSelectionState, Table } from '@tanstack/react-table'
import './index.css'

const _features = tableFeatures({
  rowPaginationFeature,
  rowSelectionFeature,
  columnFilteringFeature,
  globalFilteringFeature,
})

const columnHelper = createColumnHelper<typeof _features, Person>()

function App() {
  const rerender = React.useReducer(() => ({}), {})[1]

  const columns = React.useMemo(
    () =>
      columnHelper.columns([
        columnHelper.display({
          id: 'select',
          header: () => {
            return (
              <table.Subscribe
                source={table.atoms.rowSelection} // slice atom only — not the full derived store
                // omit selector to subscribe to the whole rowSelection slice
              >
                {() => (
                  <IndeterminateCheckbox
                    checked={table.getIsAllRowsSelected()}
                    indeterminate={table.getIsSomeRowsSelected()}
                    onChange={table.getToggleAllRowsSelectedHandler()}
                  />
                )}
              </table.Subscribe>
            )
          },
          cell: ({ row }) => (
            <div className="column-toggle-row">
              <IndeterminateCheckbox
                checked={row.getIsSelected()}
                disabled={!row.getCanSelect()}
                indeterminate={row.getIsSomeSelected()}
                onChange={row.getToggleSelectedHandler()}
              />
            </div>
          ),
        }),
        columnHelper.accessor('firstName', {
          header: 'First Name',
          cell: (info) => info.getValue(),
          footer: (props) => props.column.id,
        }),
        columnHelper.accessor((row) => row.lastName, {
          id: 'lastName',
          header: () => <span>Last Name</span>,
          cell: (info) => info.getValue(),
          footer: (props) => props.column.id,
        }),
        columnHelper.accessor('age', {
          header: () => 'Age',
          footer: (props) => props.column.id,
        }),
        columnHelper.accessor('visits', {
          header: () => <span>Visits</span>,
          footer: (props) => props.column.id,
        }),
        columnHelper.accessor('status', {
          header: 'Status',
          footer: (props) => props.column.id,
        }),
        columnHelper.accessor('progress', {
          header: 'Profile Progress',
          footer: (props) => props.column.id,
        }),
      ]),
    [],
  )

  const [data, setData] = React.useState(() => makeData(1_000))
  const refreshData = () => setData(makeData(1_000))
  const stressTest = () => setData(makeData(100_000))

  const rowSelectionAtom = useCreateAtom<RowSelectionState>({})

  const table = useTable(
    {
      _features,
      _rowModels: {
        filteredRowModel: createFilteredRowModel(filterFns),
        paginatedRowModel: createPaginatedRowModel(),
      },
      atoms: {
        rowSelection: rowSelectionAtom,
      },
      columns,
      data,
      getRowId: (row) => row.id,
      enableRowSelection: true, // enable row selection for all rows
      // enableRowSelection: row => row.original.age > 18, // or enable row selection conditionally per row
      debugTable: true,
    },
    // (state) => state, // uncomment to subscribe to the entire table state (this is how v8 used to work by default)
  )

  useTanStackTableDevtools(table, 'Row Selection Example')

  return (
    <>
      <table.Subscribe
        selector={(state) => ({
          // Store mode: multiple slices — must use table.store + explicit selector
          columnFilters: state.columnFilters,
          globalFilter: state.globalFilter,
          pagination: state.pagination,
        })}
      >
        {(state) => (
          <div className="demo-root">
            <div>
              <button
                className="demo-button demo-button-spaced"
                onClick={() => refreshData()}
              >
                Regenerate Data
              </button>
              <button
                className="demo-button demo-button-spaced"
                onClick={() => stressTest()}
              >
                Stress Test (100k rows)
              </button>
            </div>
            <div>
              <DebouncedInput
                value={state.globalFilter ?? ''}
                onChange={(value) => table.setGlobalFilter(value)}
                className="summary-panel"
                placeholder="Search all columns..."
              />
            </div>
            <div className="spacer-sm" />
            <table>
              <thead>
                {table.getHeaderGroups().map((headerGroup) => (
                  <tr key={headerGroup.id}>
                    {headerGroup.headers.map((header) => {
                      return (
                        <th key={header.id} colSpan={header.colSpan}>
                          {header.isPlaceholder ? null : (
                            <>
                              <table.FlexRender header={header} />
                              {header.column.getCanFilter() ? (
                                <div>
                                  <Filter
                                    column={header.column}
                                    table={table}
                                  />
                                </div>
                              ) : null}
                            </>
                          )}
                        </th>
                      )
                    })}
                  </tr>
                ))}
              </thead>
              <tbody>
                {table.getRowModel().rows.map((row) => {
                  return (
                    <table.Subscribe
                      key={row.id}
                      source={table.atoms.rowSelection} // same slice as table.atoms.rowSelection; external atom for clarity
                      selector={(rowSelection) => rowSelection?.[row.id]} // optional: narrow to this row so the row re-renders only when this id toggles
                    >
                      {(_isRowSelected) => (
                        <tr key={row.id}>
                          {row.getAllCells().map((cell) => {
                            return (
                              <td key={cell.id}>
                                <table.FlexRender cell={cell} />
                              </td>
                            )
                          })}
                        </tr>
                      )}
                    </table.Subscribe>
                  )
                })}
              </tbody>
              <tfoot>
                <tr>
                  <td className="cell-padding">
                    <table.Subscribe
                      source={table.atoms.rowSelection} // whole slice; footer toggles don’t need per-row projection
                      selector={(a) => a}
                    >
                      {() => (
                        <IndeterminateCheckbox
                          checked={table.getIsAllPageRowsSelected()}
                          indeterminate={table.getIsSomePageRowsSelected()}
                          onChange={table.getToggleAllPageRowsSelectedHandler()}
                        />
                      )}
                    </table.Subscribe>
                  </td>
                  <td colSpan={20}>
                    Page Rows (
                    {table.getRowModel().rows.length.toLocaleString()})
                  </td>
                </tr>
              </tfoot>
            </table>
            <div className="spacer-sm" />
            <div className="controls">
              <button
                className="demo-button demo-button-sm"
                onClick={() => table.setPageIndex(0)}
                disabled={!table.getCanPreviousPage()}
              >
                {'<<'}
              </button>
              <button
                className="demo-button demo-button-sm"
                onClick={() => table.previousPage()}
                disabled={!table.getCanPreviousPage()}
              >
                {'<'}
              </button>
              <button
                className="demo-button demo-button-sm"
                onClick={() => table.nextPage()}
                disabled={!table.getCanNextPage()}
              >
                {'>'}
              </button>
              <button
                className="demo-button demo-button-sm"
                onClick={() => table.setPageIndex(table.getPageCount() - 1)}
                disabled={!table.getCanNextPage()}
              >
                {'>>'}
              </button>
              <span className="inline-controls">
                <div>Page</div>
                <strong>
                  {(
                    table.store.state.pagination.pageIndex + 1
                  ).toLocaleString()}{' '}
                  of {table.getPageCount().toLocaleString()}
                </strong>
              </span>
              <span className="inline-controls">
                | Go to page:
                <input
                  type="number"
                  min="1"
                  max={table.getPageCount()}
                  defaultValue={table.store.state.pagination.pageIndex + 1}
                  onChange={(e) => {
                    const page = e.target.value ? Number(e.target.value) - 1 : 0
                    table.setPageIndex(page)
                  }}
                  className="page-size-input"
                />
              </span>
              <select
                value={table.store.state.pagination.pageSize}
                onChange={(e) => {
                  table.setPageSize(Number(e.target.value))
                }}
              >
                {[10, 20, 30, 40, 50].map((pageSize) => (
                  <option key={pageSize} value={pageSize}>
                    Show {pageSize}
                  </option>
                ))}
              </select>
            </div>
            <br />
            <div>
              <table.Subscribe
                selector={(state) => ({
                  numSelected: Object.keys(state.rowSelection).length,
                })}
              >
                {({ numSelected }) => <>{numSelected.toLocaleString()} of </>}
              </table.Subscribe>
              {table.getPreFilteredRowModel().rows.length.toLocaleString()}{' '}
              Total Rows Selected
            </div>
            <hr />
            <br />
            <div>
              <button
                className="demo-button demo-button-spaced"
                onClick={() => rerender()}
              >
                Force Rerender
              </button>
            </div>
            <div>
              <button
                className="demo-button demo-button-spaced"
                onClick={() =>
                  console.info(
                    'table.getSelectedRowModel().flatRows',
                    table.getSelectedRowModel().flatRows,
                  )
                }
              >
                Log table.getSelectedRowModel().flatRows
              </button>
            </div>
            <div>
              <label>Row Selection State:</label>
              <table.Subscribe selector={(state) => state}>
                {(state) => <pre>{JSON.stringify(state, null, 2)}</pre>}
              </table.Subscribe>
            </div>
          </div>
        )}
      </table.Subscribe>
    </>
  )
}

function Filter({
  column,
  table,
}: {
  column: Column<typeof _features, Person>
  table: Table<typeof _features, Person>
}) {
  const firstValue = table
    .getPreFilteredRowModel()
    .flatRows[0]?.getValue(column.id)

  return typeof firstValue === 'number' ? (
    <div className="filter-row">
      <DebouncedInput
        type="number"
        value={((column.getFilterValue() as any)?.[0] ?? '') as string}
        onChange={(value) =>
          column.setFilterValue((old: any) => [value, old?.[1]])
        }
        placeholder={`Min`}
        className="filter-input"
      />
      <DebouncedInput
        type="number"
        value={((column.getFilterValue() as any)?.[1] ?? '') as string}
        onChange={(value) =>
          column.setFilterValue((old: any) => [old?.[0], value])
        }
        placeholder={`Max`}
        className="filter-input"
      />
    </div>
  ) : (
    <DebouncedInput
      type="text"
      value={(column.getFilterValue() ?? '') as string}
      onChange={(value) => column.setFilterValue(value)}
      placeholder={`Search...`}
      className="filter-select"
    />
  )
}

// A debounced input react component
function DebouncedInput({
  value: initialValue,
  onChange,
  debounce = 500,
  ...props
}: {
  value: string | number
  onChange: (value: string | number) => void
  debounce?: number
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'>) {
  const [value, setValue] = React.useState(initialValue)

  React.useEffect(() => {
    setValue(initialValue)
  }, [initialValue])

  React.useEffect(() => {
    const timeout = setTimeout(() => {
      onChange(value)
    }, debounce)

    return () => clearTimeout(timeout)
  }, [value])

  return (
    <input
      {...props}
      value={value}
      onChange={(e) => setValue(e.target.value)}
    />
  )
}

function IndeterminateCheckbox({
  indeterminate,
  className = '',
  ...rest
}: { indeterminate?: boolean } & HTMLProps<HTMLInputElement>) {
  const ref = React.useRef<HTMLInputElement>(null!)

  React.useEffect(() => {
    if (typeof indeterminate === 'boolean') {
      ref.current.indeterminate = !rest.checked && indeterminate
    }
  }, [ref, indeterminate])

  return (
    <input
      type="checkbox"
      ref={ref}
      className={className + ' sortable-header'}
      {...rest}
    />
  )
}

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

ReactDOM.createRoot(rootElement).render(
  <React.StrictMode>
    <App />
    <TanStackDevtools plugins={[tableDevtoolsPlugin()]} />
  </React.StrictMode>,
)