TanStack Table in React: Installation, Setup, and a Real Interactive Example
TanStack Table is a headless React table library: it gives you the logic (row models, sorting,
filtering, pagination), but you bring your own markup and styles. If you’ve ever wanted a
React data table headless approach where the “table brain” is separated from the “table face”,
you’re in the right place.
This guide is a practical
TanStack Table React walkthrough: we’ll do
TanStack Table installation, a clean TanStack Table setup, and then build one cohesive
React interactive table with TanStack Table sorting, TanStack Table filtering,
and TanStack Table pagination.
If you want a second perspective after reading, this
TanStack Table tutorial
is a solid companion piece. Here we’ll go a bit more “copy-paste and ship” (without pretending styling is someone else’s problem).
What you’re actually getting: headless tables vs “data grid” components
When people search for a React table component, they often mean “give me a ready-made UI.”
TanStack Table doesn’t do that. It behaves more like a table engine: you define columns and data,
then it computes header groups, row models, and cell values. You render the results however you want:
native <table>, div-based layouts, or a design-system wrapper.
This is why it’s frequently compared to a React data grid. A data grid usually ships with
UI, styling, and a thick feature layer. TanStack Table ships with logic and composability. The tradeoff is fair:
you keep full UI control, but you must render sort toggles, filter inputs, empty states, and pagination controls yourself.
In practice, headless is a superpower. Your product team wants a “weird” table layout? Fine.
Your design system demands custom components? Great. You won’t fight someone else’s DOM structure.
And if you only need 20% of typical grid features, you don’t pay (in complexity) for the other 80%.
- Choose TanStack Table if you want full UI control, predictable logic, and composability.
- Choose a UI grid if you want out-of-the-box styling and can live with its structure and constraints.
TanStack Table installation and setup (React)
The package you need is @tanstack/react-table. This is the “React adapter” for the core table engine.
Installation is straightforward, and you don’t need peer UI dependencies. That’s the whole point: headless.
Run one of the following. (Yes, this section exists because “it didn’t install” is still a top-tier productivity killer.)
npm i @tanstack/react-table
# or
yarn add @tanstack/react-table
# or
pnpm add @tanstack/react-table
Conceptually, the TanStack Table setup has four moving parts:
(1) your data array, (2) your column definitions, (3) table state (sorting, filters, pagination),
and (4) row models that transform data into what you render. If you understand those, you can build
almost any React table TanStack use case without cargo-culting snippets.
TanStack Table example: a complete interactive table (sorting + filtering + pagination)
Below is a single-component TanStack Table example that wires the “big three” together:
TanStack Table sorting, TanStack Table filtering, and TanStack Table pagination.
It’s intentionally boring in styling—because your app isn’t boring, and you’ll style it your way.
Two implementation details prevent common headaches:
use useMemo for stable columns and data references, and keep
table state controlled in React. Uncontrolled state can be fine for prototypes, but controlled state
makes feature interactions (and URL sync) much easier later.
This is a practical React table component tutorial approach: copy it, run it, then iterate.
Once it works, swap the markup for your design system and keep the TanStack logic intact.
import React, { useMemo, useState } from "react";
import {
ColumnDef,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
SortingState,
useReactTable,
} from "@tanstack/react-table";
type Person = {
firstName: string;
lastName: string;
age: number;
status: "single" | "relationship" | "complicated";
};
const defaultData: Person[] = [
{ firstName: "Ada", lastName: "Lovelace", age: 36, status: "relationship" },
{ firstName: "Grace", lastName: "Hopper", age: 85, status: "single" },
{ firstName: "Alan", lastName: "Turing", age: 41, status: "complicated" },
{ firstName: "Katherine", lastName: "Johnson", age: 101, status: "relationship" },
];
export function PeopleTable() {
const data = useMemo(() => defaultData, []);
const columns = useMemo<ColumnDef<Person>[]>(
() => [
{
accessorKey: "firstName",
header: "First name",
cell: (info) => info.getValue(),
},
{
accessorKey: "lastName",
header: "Last name",
cell: (info) => info.getValue(),
},
{
accessorKey: "age",
header: "Age",
cell: (info) => info.getValue(),
},
{
accessorKey: "status",
header: "Status",
cell: (info) => info.getValue(),
},
],
[]
);
// Table state (controlled)
const [sorting, setSorting] = useState<SortingState>([]);
const [globalFilter, setGlobalFilter] = useState("");
const table = useReactTable({
data,
columns,
state: {
sorting,
globalFilter,
},
onSortingChange: setSorting,
onGlobalFilterChange: setGlobalFilter,
// Row models (the "pipeline")
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
return (
<div style={{ maxWidth: 900 }}>
<div style={{ display: "flex", gap: 12, alignItems: "center", marginBottom: 12 }}>
<label>
Search:
<input
value={globalFilter ?? ""}
onChange={(e) => setGlobalFilter(e.target.value)}
placeholder="Type to filter..."
/>
</label>
<span style={{ opacity: 0.8 }}>
Rows: {table.getFilteredRowModel().rows.length}
</span>
</div>
<table style={{ width: "100%", borderCollapse: "collapse" }}>
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => {
const canSort = header.column.getCanSort();
const sortDir = header.column.getIsSorted(); // false | "asc" | "desc"
return (
<th
key={header.id}
onClick={canSort ? header.column.getToggleSortingHandler() : undefined}
style={{
textAlign: "left",
borderBottom: "1px solid #ddd",
padding: "10px 8px",
cursor: canSort ? "pointer" : "default",
userSelect: "none",
whiteSpace: "nowrap",
}}
scope="col"
aria-sort={
sortDir === "asc" ? "ascending" : sortDir === "desc" ? "descending" : "none"
}
>
{flexRender(header.column.columnDef.header, header.getContext())}
{canSort && (
<span style={{ marginLeft: 6, opacity: 0.7 }}>
{sortDir === "asc" ? "▲" : sortDir === "desc" ? "▼" : "↕"}
</span>
)}
</th>
);
})}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => (
<tr key={row.id}>
{row.getVisibleCells().map((cell) => (
<td key={cell.id} style={{ padding: "10px 8px", borderBottom: "1px solid #f0f0f0" }}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
{table.getRowModel().rows.length === 0 && (
<tr>
<td colSpan={table.getAllColumns().length} style={{ padding: 12, opacity: 0.8 }}>
No results. Try a different search.
</td>
</tr>
)}
</tbody>
</table>
<div style={{ display: "flex", gap: 10, alignItems: "center", marginTop: 12 }}>
<button onClick={() => table.setPageIndex(0)} disabled={!table.getCanPreviousPage()}>
{"<<"}
</button>
<button onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>
Previous
</button>
<button onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
Next
</button>
<button
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
{">>"}
</button>
<span style={{ marginLeft: 8 }}>
Page{" "}
<strong>
{table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
</strong>
</span>
<select
value={table.getState().pagination.pageSize}
onChange={(e) => table.setPageSize(Number(e.target.value))}
style={{ marginLeft: "auto" }}
>
{[5, 10, 20, 50].map((size) => (
<option key={size} value={size}>
Show {size}
</option>
))}
</select>
</div>
</div>
);
}
How sorting, filtering, and pagination really work (so you can debug them fast)
TanStack Table features are “activated” by adding the relevant row model. Think of row models as a pipeline:
core produces the baseline rows, then sorted reorders them, filtered
removes non-matching rows, and paginated slices the result to a page window. You can add or remove
stages depending on your needs, and that’s why it scales from simple tables to complex apps.
Sorting is controlled through SortingState and onSortingChange. When you click a header,
TanStack updates sorting state, then getSortedRowModel() produces a derived row set. If sorting “does nothing,”
the usual culprits are: you forgot getSortedRowModel, your column can’t sort (getCanSort() is false),
or your header click handler isn’t attached to the actual header element.
Filtering comes in multiple flavors. The example uses globalFilter (a single search box).
You can also add column filters per field. If you’re aiming for a “real” React data grid experience,
you’ll likely end up combining: global search, column-specific filters, and server-side filtering for large datasets.
The nice part is you can keep the same UI and swap the data source when you outgrow client-side filtering.
Pagination is similar: client-side pagination just slices already-loaded rows. For large datasets you’ll often switch
to server-side pagination (manual pagination), where you fetch the next page from an API and tell the table how many pages exist.
TanStack supports that pattern cleanly, but it’s a separate setup (and you’ll want URL syncing).
- Debug trick: log
table.getState()and the length of each row model when something looks off. - Performance trick: keep
dataandcolumnsmemoized to avoid unnecessary recalculations.
Common “React table library” decisions (and a few sharp edges)
If you’re evaluating a React table library, TanStack Table is usually the right choice when you want control.
But control has a price: you own the UI behavior. That means you should intentionally design keyboard navigation,
focus states, and accessible sorting indicators (the example uses aria-sort on headers).
Another decision point is whether your table is “component-like” or “feature-like.”
A classic React table component is a drop-in widget. TanStack Table is better treated as a feature module:
a small set of UI components (Table, HeaderCell, Pagination, Filters) backed by a consistent table instance.
That mindset prevents the usual mess where every screen has a slightly different table implementation.
Finally, if you’re building a big React interactive table (thousands of rows), plan for virtualization.
TanStack Table focuses on table logic; virtualization is typically handled by libraries like TanStack Virtual.
Don’t try to brute-force 50,000 DOM rows and then blame the table library—it’s innocent.
If you want the official starting points and API references, use the
TanStack Table tutorial
docs as your source of truth, and treat blog posts (including this one) as implementation recipes.
FAQ
What is TanStack Table in React?
TanStack Table is a headless table engine for React: it provides table logic (row models, sorting, filtering, pagination),
while you render the UI yourself using any markup and styling approach.
How do I add sorting and filtering to TanStack Table?
Use controlled state (e.g., sorting, globalFilter) and enable the corresponding row models:
getSortedRowModel() and getFilteredRowModel(). Then attach header click handlers for sorting and inputs for filtering.
Is TanStack Table a data grid?
It’s closer to a data-grid engine than a prebuilt grid component. You get the behavior and data transformations,
but you must build the UI (headers, filters, pagination controls, styling) yourself.
Semantic core (expanded, clustered)
Primary (core intent)
TanStack Table React; React table TanStack; TanStack Table tutorial; React table component tutorial;
TanStack Table example; TanStack Table setup; TanStack Table installation.
Feature clusters (high intent)
TanStack Table sorting; TanStack Table filtering; TanStack Table pagination; React interactive table;
React data table headless; React table component; React table library; React data grid.
Secondary / LSI / synonyms
headless table; table engine; column definitions; row model; global filter; column filters; controlled state;
client-side pagination; server-side pagination; manual pagination; sorting state; filtering state; accessible table;
aria-sort; performance; memoized columns; virtualization; TanStack Virtual; data table in React; build a data table component.
Question-style (voice search friendly)
How do I install TanStack Table in React?; How do I add sorting in TanStack Table?;
How do I filter a TanStack Table?; How do I paginate TanStack Table?; Is TanStack Table a data grid?;
What does headless table mean in React?
Popular question pool (for PAA/featured snippets)
1) What is TanStack Table and why is it headless?
2) How do I install @tanstack/react-table?
3) How do I define columns in TanStack Table v8?
4) How do I add sorting to TanStack Table?
5) How do I add global filtering vs column filtering?
6) How do I implement pagination (client vs server)?
7) Is TanStack Table better than a data grid component?
8) How do I improve performance with large datasets?
9) How do I integrate TanStack Table with Material UI/Chakra/shadcn?
10) How do I virtualize rows in React tables?
Selected for final FAQ (top relevance)
• What is TanStack Table in React?
• How do I add sorting and filtering to TanStack Table?
• Is TanStack Table a data grid?