kb.erickguedes.com
TanStack: Ecossistema de Bibliotecas

TanStack Table: Data Grids Headless

Aula 3 de 5

Construindo Tabelas com TanStack Table

O TanStack Table (antigo React Table) é uma biblioteca headless para criar data grids. Ela controla estado lógico (sorting, filtering, pagination, selection) enquanto você controla a renderização.

Configuração Inicial

npm install @tanstack/react-table

Tabela Básica

import { useReactTable, getCoreRowModel, flexRender, createColumnHelper } from '@tanstack/react-table'

type User = { id: string; name: string; email: string; role: string }

const data: User[] = [
  { id: '1', name: 'Ana Silva', email: '[email protected]', role: 'Admin' },
  { id: '2', name: 'Carlos Lima', email: '[email protected]', role: 'Editor' },
]

const columnHelper = createColumnHelper<User>()

const columns = [
  columnHelper.accessor('name', { header: 'Nome', cell: info => info.getValue() }),
  columnHelper.accessor('email', { header: 'E-mail' }),
  columnHelper.accessor('role', { header: 'Função' }),
]

function UserTable() {
  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
  })

  return (
    <table>
      <thead>
        {table.getHeaderGroups().map(headerGroup => (
          <tr key={headerGroup.id}>
            {headerGroup.headers.map(header => (
              <th key={header.id}>
                {flexRender(header.column.columnDef.header, header.getContext())}
              </th>
            ))}
          </tr>
        ))}
      </thead>
      <tbody>
        {table.getRowModel().rows.map(row => (
          <tr key={row.id}>
            {row.getVisibleCells().map(cell => (
              <td key={cell.id}>
                {flexRender(cell.column.columnDef.cell, cell.getContext())}
              </td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  )
}

Sorting e Filtering

import { getSortedRowModel, getFilteredRowModel } from '@tanstack/react-table'

function Table() {
  const [sorting, setSorting] = useState<SortingState>([])
  const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])

  const table = useReactTable({
    data,
    columns,
    state: { sorting, columnFilters },
    onSortingChange: setSorting,
    onColumnFiltersChange: setColumnFilters,
    getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    enableColumnFilters: true,
  })

  return (
    <div>
      <input
        placeholder="Filtrar nome..."
        value={(table.getColumn('name')?.getFilterValue() as string) ?? ''}
        onChange={e => table.getColumn('name')?.setFilterValue(e.target.value)}
      />
      <table>
        <thead>
          {table.getHeaderGroups().map(headerGroup => (
            <tr key={headerGroup.id}>
              {headerGroup.headers.map(header => (
                <th key={header.id} onClick={header.column.getToggleSortingHandler()}>
                  {flexRender(header.column.columnDef.header, header.getContext())}
                  {{ asc: ' 🔼', desc: ' 🔽' }[header.column.getIsSorted() as string] ?? null}
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody>{/* ... */}</tbody>
      </table>
    </div>
  )
}

Paginação

import { getPaginationRowModel } from '@tanstack/react-table'

const table = useReactTable({
  data,
  columns,
  getCoreRowModel: getCoreRowModel(),
  getPaginationRowModel: getPaginationRowModel(),
  initialState: { pagination: { pageSize: 10 } },
})

// Controles
<div>
  <button onClick={() => table.firstPage()} disabled={!table.getCanPreviousPage()}>
    {'<<'}
  </button>
  <button onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>
    {'<'}
  </button>
  <span>
    Página {table.getState().pagination.pageIndex + 1} de {table.getPageCount()}
  </span>
  <button onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
    {'>'}
  </button>
  <button onClick={() => table.lastPage()} disabled={!table.getCanNextPage()}>
    {'>>'}
  </button>
</div>

Row Selection e Column Visibility

import { getCoreRowModel } from '@tanstack/react-table'

const table = useReactTable({
  data,
  columns,
  getCoreRowModel: getCoreRowModel(),
  enableRowSelection: true,
  enableColumnVisibility: true,
})

// Checkbox no header da seleção
columnHelper.display({
  id: 'select',
  header: ({ table }) => (
    <input
      type="checkbox"
      checked={table.getIsAllRowsSelected()}
      onChange={table.getToggleAllRowsSelectedHandler()}
    />
  ),
  cell: ({ row }) => (
    <input
      type="checkbox"
      checked={row.getIsSelected()}
      onChange={row.getToggleSelectedHandler()}
    />
  ),
})

// Toggle visibilidade
<table.ColumnVisibility>
  {table.getAllColumns().map(col => (
    <label key={col.id}>
      <input
        type="checkbox"
        checked={col.getIsVisible()}
        onChange={col.getToggleVisibilityHandler()}
      />
      {col.columnDef.header as string}
    </label>
  ))}
</table.ColumnVisibility>

Virtualização com @tanstack/react-virtual

import { useReactTable, getCoreRowModel } from '@tanstack/react-table'
import { useVirtualizer } from '@tanstack/react-virtual'

function VirtualizedTable() {
  const parentRef = useRef<HTMLDivElement>(null)

  const table = useReactTable({
    data: largeDataset,
    columns,
    getCoreRowModel: getCoreRowModel(),
  })

  const { rows } = table.getRowModel()

  const virtualizer = useVirtualizer({
    count: rows.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 40,
    overscan: 10,
  })

  return (
    <div ref={parentRef} style={{ height: '500px', overflow: 'auto' }}>
      <div style={{ height: `${virtualizer.getTotalSize()}px` }}>
        {virtualizer.getVirtualItems().map(virtualRow => {
          const row = rows[virtualRow.index]
          return (
            <div
              key={row.id}
              style={{
                position: 'absolute',
                top: 0,
                transform: `translateY(${virtualRow.start}px)`,
                height: `${virtualRow.size}px`,
              }}
            >
              {row.getVisibleCells().map(cell => (
                <span key={cell.id}>
                  {flexRender(cell.column.columnDef.cell, cell.getContext())}
                </span>
              ))}
            </div>
          )
        })}
      </div>
    </div>
  )
}

Células Editáveis

function EditableCell({ getValue, row, column, table }: CellContext<User, string>) {
  const initialValue = getValue()
  const [value, setValue] = useState(initialValue)

  const onBlur = () => {
    table.options.meta?.updateData(row.index, column.id, value)
  }

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

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

Lab: Data Grid Completo

npm create vite@latest table-demo -- --template react-ts
cd table-demo
npm install @tanstack/react-table @tanstack/react-virtual
// Construa:
// 1. Tabela com 1000+ linhas simuladas
// 2. Sorting multi-coluna
// 3. Filtro global + filtro por coluna
// 4. Paginação com pageSize configurável
// 5. Seleção de linhas com ações em lote
// 6. Virtualização para performance
// 7. Uma coluna editável inline

TanStack Table é a camada lógica perfeita para data grids — você controla o visual, a biblioteca controla o estado complexo de tabelas.