This episode builds your first table with TanStack Table: defining columns with columnHelper, using useReactTable with a row model, rendering cells with flexRender, then adding sorting, filtering, and pagination.

TanStack Query manages server data; TanStack Table manages how that data is displayed in rows and columns. After episode 5, you have data from a query — now it's time to present it in a table that can be sorted, filtered, and paginated.
Episode 6 builds the first table from scratch: defining columns as data, using useReactTable, rendering through flexRender, then enabling sorting, filtering, and pagination through row models.
Important to remember from episode 2: TanStack Table ships no visual styling. You render the table with plain HTML elements — this is the headless philosophy in its most tangible form.
Columns are defined as data with createColumnHelper. The data type is provided as a generic so accessors are checked by TypeScript:
import { createColumnHelper } from "@tanstack/react-table"
type Pengguna = { id: number; nama: string; peran: string }
const data: Pengguna[] = [
{ id: 1, nama: "Arman", peran: "Engineer" },
{ id: 2, nama: "Dwi", peran: "Designer" },
]
const columnHelper = createColumnHelper<Pengguna>()
const columns = [
columnHelper.accessor("id", { header: "ID" }),
columnHelper.accessor("nama", { header: "Nama" }),
columnHelper.accessor("peran", { header: "Peran" }),
]createColumnHelper<Pengguna>() binds the helper to the data type, so columnHelper.accessor("nama" is validated by the compiler. header is the column label; later it can be replaced with a function or component.
useReactTable accepts data, columns, and one or more row models. The base model is getCoreRowModel:
import { useReactTable, getCoreRowModel } from "@tanstack/react-table"
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
})useReactTable({ data, columns }) builds the core table. getCoreRowModel() computes the base rows. Other models — sorting, filtering, pagination — just get added to the same list.
Rendering uses table.getHeaderGroups(), table.getRowModel().rows, and flexRender to evaluate the header and cell definitions:
<table>
<thead>
{table.getHeaderGroups().map((hg) => (
<tr key={hg.id}>
{hg.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>flexRender(header.column.columnDef.header, header.getContext()) evaluates a header definition into a real element. header.getContext() and cell.getContext() carry the table state the cell needs — this is the bridge between definitions and rendered output.
The three basic features are enabled simply by adding models to useReactTable:
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
})
const { pageIndex, pageSize } = table.getState().paginationModels run in sequence: the core model produces rows, filtering narrows them, sorting orders them, and pagination slices them into pages. table.getState().pagination holds the active page and page size.
Sorting is triggered by a button on the header, pagination by page navigation buttons:
<button onClick={header.column.getToggleSortingHandler()}>
{header.column.getIsSorted() === "asc" ? "Naik" : "Turun"}
</button><button onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>
Sebelumnya
</button>
<button onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
Berikutnya
</button>header.column.getToggleSortingHandler() toggles the sort direction when clicked. table.previousPage() and table.nextPage() move between pages, while getCanPreviousPage and getCanNextPage disable the buttons at the edges of the range.
Info
Basic filtering can be enabled with column.setFilterValue through an input on each header. Episode 9 covers advanced filtering, grouping, and dynamic columns.
Episode 6 completed your first table: columns defined as data with columnHelper, useReactTable combining data and row models, manual rendering with flexRender, and sorting, filtering, and pagination enabled through additional models.
Key takeaways:
In the next episode, episode 7, we'll discuss routing and navigation — building a route tree with TanStack Router, nested routes and layouts, data loading with loaders, and navigation with Link and route state. You'll connect the episode 6 table to a URL that's truly navigable!