Technology handbook
TanStack Table
Headless row models, columns, sorting, filtering, grouping, selection, pinning and virtualization.
- Topics
- 32
- Sections
- 5
Table hooks & state
Row model functions
getCoreRowModel and its siblings are what enable sorting, filtering, grouping and pagination.
- Each row model is opt-in, so a plain table pays for nothing it does not use.
- They run in a fixed order: filter, sort, group, expand, then paginate.
- A feature that silently does nothing is usually a missing row model.
Controlled table state
Lift sorting, filtering or pagination into your own state and pass it back in.
- Lifting state lets a URL or a store own the current sort and page.
- The table stops tracking what you control, so the value must come back in.
- Partial control is allowed: lift only the slices the application cares about.
Manual server-side state
Turn on manualSorting, manualFiltering or manualPagination when the server does the work.
- Manual mode tells the table the rows it received are already sorted or paged.
- Without it the table sorts the current page again and the order looks wrong.
- Supply the total row count yourself, since the table cannot infer it.
Header and cell renderers
A header or cell definition may be a value, a function or a component, and all three render the same way.
- A definition may be a plain value, an element or a function of the context.
- One render helper handles all three, so call sites need no branching.
- The function form receives the row, so a cell can render an action for it.
Column helpers and typing
Define columns through the typed helper so accessors and cell values stay in step.
- The helper infers the cell type from the accessor it is given.
- A renamed data field breaks the column definition rather than rendering blank.
- Sorting and filtering functions are typed against the same value.
Reading header groups and rows
Draw the table from getHeaderGroups and getRowModel rather than from your source array.
- Header groups already account for nesting, pinning and visibility.
- Rendering the source array directly bypasses every feature the table adds.
- The row model is what reflects the current sort, filter and page.
Table state slices
Subscribe to one piece of table state instead of re-rendering on every table change.
- Subscribing to one slice keeps a header from re-rendering on a filter change.
- It matters once a table holds hundreds of cells.
- Read the slice where it is used rather than threading it from the top.
Row identity across state
Give each row a stable id, or selection and expansion follow positions instead of rows.
- A stable id keeps a selection attached to a record across a re-sort.
- Index-based identity silently selects a different row when the order changes.
- The same id drives expansion, pinning and optimistic updates.
Table fundamentals
useReactTable
Create a table instance from data, columns and the row models a view needs.
- The instance is the single object every header, row and cell is drawn from.
- It holds state, row models and the feature configuration together.
- The instance is recreated per render but keeps its state across renders.
Column definitions
Describe an accessor, a header and a cell renderer for each column.
- A column says where its value comes from and how it is displayed.
- Definitions are data, so they can be generated from a schema.
- Per-column options carry sorting, filtering and sizing behaviour.
Accessor and display columns
Separate columns that read a value from columns that render controls.
- An accessor column has a value and can therefore be sorted and filtered.
- A display column holds checkboxes or buttons and has no value at all.
- Giving a display column an accessor produces a column that sorts on nothing.
Row models
Add only the models a table uses, since each one costs work on every change.
- Every added model recomputes on each data or state change.
- A table without pagination should not include the pagination model.
- The cost shows up on large datasets long before it shows up on small ones.
flexRender
Render a header or cell definition that may be a value, an element or a function.
- The helper resolves whichever of the three definition forms was used.
- It passes the header or cell context through to a function definition.
- Calling a definition directly breaks as soon as someone passes a component.
Stable data and columns
Memoize both so the instance does not discard its state each render.
- A new array identity each render makes the table rebuild its row models.
- Column definitions are static and belong outside the component or in a memo.
- The symptom is a table that loses its sort or selection while typing.
Sorting & filtering
Sorting state
Track which columns sort, in what direction, and whether multiple sorts combine.
- The state is a list, which is what makes multi-column sorting expressible.
- Direction and null placement are part of the column, not of the click handler.
- Toggling cycles ascending, descending and off unless that is configured away.
Custom sort functions
Order values the default comparator cannot, such as localized text or nulls.
- Locale-aware text sorting needs a comparator, not the default ordering.
- Decide explicitly where empty values belong rather than letting them scatter.
- A comparator registered by name can be reused across columns.
Column filters
Filter one column at a time with a predicate the column owns.
- Each column owns its predicate, so a date column can filter by range.
- Filters combine, so two columns narrow the rows together.
- The filter value is state, which means it can be restored from a URL.
Global filter
Search across columns with a single input and a shared matching rule.
- One input matches across the columns that opt into it.
- The matching rule is yours: substring, fuzzy or normalised.
- Debounce the input, since every keystroke recomputes the row model.
Faceted values
Offer filter options drawn from the data actually present in the column.
- Facets are built from the data present, so a filter never offers an empty option.
- Counts beside each option tell the reader what a choice will cost.
- Facets must be computed after other filters if they should narrow together.
Manual filtering and sorting
Hand the work to a server and tell the table not to recompute it.
- The server does the work and the table just renders what came back.
- State changes become query parameters rather than local recomputation.
- Forgetting the manual flag makes the table re-sort the page it was given.
Grouping & aggregation
Grouping state
Collapse rows under a grouping column and expand them on demand.
- Grouping by a column collapses its rows under one header row.
- The grouped column is removed from the leaf rows that sit beneath it.
- Grouping runs before pagination, which changes what a page contains.
Aggregation functions
Summarize a group with a sum, mean or custom reducer.
- An aggregate is computed per group, not per visible page.
- Sum, mean and count cover most cases; a reducer covers the rest.
- Aggregating a formatted string instead of its number is the usual bug.
Expanding
Control which groups or sub-rows are open, including nested hierarchies.
- Expansion state is keyed by row id, so it survives a re-sort.
- The same mechanism serves group rows and nested sub-rows.
- Expanding everything on a large table defeats the point of grouping.
Sub-rows
Render tree data by pointing the table at each row's children.
- A tree is described by pointing each row at its children.
- Depth is available per row, which is what indentation is drawn from.
- Filtering a tree has to decide whether a matching child keeps its parent.
Aggregated cell rendering
Show a different cell for a group row than for a leaf row.
- A group row usually shows a total where a leaf shows a value.
- Placeholder cells appear where a grouped column has nothing to show.
- Rendering the same cell for both makes a summary row read as a record.
Selection, layout & scale
Row selection
Track selected rows by a stable row id rather than by index.
- Selection is a map of row id to boolean, not a list of indexes.
- Select-all across pages is a different feature from select-all on this page.
- Keeping selection through a filter needs a decision about hidden rows.
Column visibility
Let a reader hide columns and persist that choice with the view.
- Hidden columns stay in the definitions and can be restored.
- Persisting the choice is what makes a wide table usable on a second visit.
- A hidden column still filters unless it is excluded deliberately.
Column ordering
Reorder columns without changing the underlying definitions.
- Order is a list of column ids kept separately from the definitions.
- Pinned columns sit outside that order at their own edge.
- Drag handles are yours to build; the table only tracks the resulting order.
Column sizing and resizing
Size columns and handle a resize interaction in your own markup.
- The table tracks widths; the markup decides how they are applied.
- Resizing updates on every pointer move, so it needs to stay cheap.
- A minimum width keeps a dragged column from collapsing to nothing.
Column and row pinning
Freeze columns or rows at an edge while the rest scrolls.
- A pinned column is rendered in its own group at the left or right.
- Sticky positioning in the markup is what actually freezes it on screen.
- Pinned rows are excluded from the scrolling body, so counts shift.
Virtualized rows
Render only visible rows for large datasets while keeping the row model intact.
- Only the visible window is rendered while the row model stays complete.
- Sorting and filtering still see every row, because they run on the model.
- Row height estimation is what keeps the scrollbar honest.
Sources
- TanStack Table official documentation