Fix: TanStack Table Not Working — Sorting Not Triggering, Filters Ignored, or Pagination Showing Wrong Data
Quick Answer
How to fix TanStack Table (React Table v8) issues — column definitions, server-side sorting and filtering, row selection, virtual rows with TanStack Virtual, and v7 to v8 migration errors.
The Problem
Clicking a column header to sort does nothing:
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
});
// Clicking headers — no sort, no re-renderOr the filter input updates but the table rows don’t change:
const [globalFilter, setGlobalFilter] = useState('');
const table = useReactTable({
data,
columns,
state: { globalFilter },
onGlobalFilterChange: setGlobalFilter,
getCoreRowModel: getCoreRowModel(),
});
// Filter state updates but rows are unchangedOr pagination shows the same rows on every page:
const table = useReactTable({
data: allRows, // 1000 rows
columns,
getPaginationRowModel: getPaginationRowModel(),
getCoreRowModel: getCoreRowModel(),
});
// Page 2 shows identical rows to page 1Or after upgrading from v7 to v8, types break everywhere:
// v7
const { rows } = useTable({ columns, data });
// Error: Property 'useTable' does not existWhy This Happens
TanStack Table v8 requires explicit opt-in for every feature:
- Row models must be explicitly registered —
getCoreRowModel()is the only required model. Sorting, filtering, and pagination each need their own model (getSortedRowModel,getFilteredRowModel,getPaginationRowModel) plus the corresponding state and event handler. statemust be controlled — features like sorting require you to declare both the state object and theon*Changehandler. Declaring one without the other leaves the feature in a broken half-controlled state.- v8 is a complete rewrite —
useTable,useSortBy,useFilters, etc. are gone. Everything is now in a singleuseReactTablehook with a composable API. Column definitions usecolumnHelperand the type system is entirely different. - Client-side vs server-side modes — by default, TanStack Table processes all data client-side. For server-side sorting/filtering, you must pass
manualSorting: true,manualFiltering: true, and handle the logic yourself.
Fix 1: Set Up useReactTable Correctly
import {
useReactTable,
getCoreRowModel,
getSortedRowModel,
getFilteredRowModel,
getPaginationRowModel,
createColumnHelper,
flexRender,
} from '@tanstack/react-table';
import { useState } from 'react';
interface User {
id: number;
name: string;
email: string;
role: 'user' | 'admin';
createdAt: string;
}
const columnHelper = createColumnHelper<User>();
const columns = [
columnHelper.accessor('id', {
header: 'ID',
cell: info => info.getValue(),
}),
columnHelper.accessor('name', {
header: 'Name',
cell: info => info.getValue(),
enableSorting: true,
enableColumnFilter: true,
}),
columnHelper.accessor('email', {
header: 'Email',
}),
columnHelper.accessor('role', {
header: 'Role',
filterFn: 'equals', // Exact match for enum columns
}),
columnHelper.display({
id: 'actions',
header: 'Actions',
cell: ({ row }) => (
<button onClick={() => handleEdit(row.original)}>Edit</button>
),
}),
];
function UserTable({ data }: { data: User[] }) {
const [sorting, setSorting] = useState([]);
const [columnFilters, setColumnFilters] = useState([]);
const [globalFilter, setGlobalFilter] = useState('');
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 });
const [rowSelection, setRowSelection] = useState({});
const table = useReactTable({
data,
columns,
state: {
sorting,
columnFilters,
globalFilter,
pagination,
rowSelection,
},
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onGlobalFilterChange: setGlobalFilter,
onPaginationChange: setPagination,
onRowSelectionChange: setRowSelection,
// Row models — register only what you use
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
// Row count for pagination (required for server-side)
// rowCount: serverTotalRows,
});
return (
<div>
<input
value={globalFilter}
onChange={e => setGlobalFilter(e.target.value)}
placeholder="Search all columns..."
/>
<table>
<thead>
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th key={header.id}>
{header.isPlaceholder ? null : (
<div
style={{ cursor: header.column.getCanSort() ? 'pointer' : 'default' }}
onClick={header.column.getToggleSortingHandler()}
>
{flexRender(header.column.columnDef.header, header.getContext())}
{header.column.getIsSorted() === 'asc' ? ' ↑'
: header.column.getIsSorted() === 'desc' ? ' ↓'
: null}
</div>
)}
</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>
<div>
<button onClick={() => table.firstPage()} disabled={!table.getCanPreviousPage()}>«</button>
<button onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>‹</button>
<span>Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}</span>
<button onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>›</button>
<button onClick={() => table.lastPage()} disabled={!table.getCanNextPage()}>»</button>
<select
value={table.getState().pagination.pageSize}
onChange={e => table.setPageSize(Number(e.target.value))}
>
{[10, 20, 50, 100].map(size => (
<option key={size} value={size}>Show {size}</option>
))}
</select>
</div>
</div>
);
}Fix 2: Add Row Selection
import { createColumnHelper } from '@tanstack/react-table';
// Add a checkbox column
const columns = [
{
id: 'select',
header: ({ table }) => (
<input
type="checkbox"
checked={table.getIsAllPageRowsSelected()}
onChange={table.getToggleAllPageRowsSelectedHandler()}
ref={el => {
if (el) el.indeterminate = table.getIsSomePageRowsSelected();
}}
/>
),
cell: ({ row }) => (
<input
type="checkbox"
checked={row.getIsSelected()}
disabled={!row.getCanSelect()}
onChange={row.getToggleSelectedHandler()}
/>
),
},
// ...other columns
];
// Table config
const table = useReactTable({
data,
columns,
state: { rowSelection },
onRowSelectionChange: setRowSelection,
enableRowSelection: true, // Enable for all rows
// enableRowSelection: row => row.original.role !== 'admin', // Conditional
getCoreRowModel: getCoreRowModel(),
});
// Get selected rows
const selectedRows = table.getSelectedRowModel().rows;
const selectedData = selectedRows.map(row => row.original);
// Bulk action
<button
disabled={selectedRows.length === 0}
onClick={() => handleBulkDelete(selectedData)}
>
Delete {selectedRows.length} selected
</button>Fix 3: Server-Side Data
For large datasets, handle sorting/filtering/pagination on the server:
function ServerSideTable() {
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 });
// Fetch from server whenever state changes
const { data, isLoading } = useQuery({
queryKey: ['users', { sorting, columnFilters, pagination }],
queryFn: () => fetchUsers({
page: pagination.pageIndex,
pageSize: pagination.pageSize,
sortBy: sorting[0]?.id,
sortDir: sorting[0]?.desc ? 'desc' : 'asc',
filters: columnFilters,
}),
});
const table = useReactTable({
data: data?.rows ?? [],
columns,
state: { sorting, columnFilters, pagination },
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onPaginationChange: setPagination,
// Tell TanStack Table that the server handles these
manualSorting: true,
manualFiltering: true,
manualPagination: true,
rowCount: data?.totalCount, // Required for page count calculation
getCoreRowModel: getCoreRowModel(),
});
return (
<div>
{isLoading && <span>Loading...</span>}
{/* Table rendering */}
</div>
);
}Fix 4: Custom Filter Functions
import { FilterFn, filterFns } from '@tanstack/react-table';
// Custom date range filter
const dateRangeFilter: FilterFn<any> = (row, columnId, filterValue) => {
const [start, end] = filterValue as [Date, Date];
const date = new Date(row.getValue(columnId));
if (start && date < start) return false;
if (end && date > end) return false;
return true;
};
dateRangeFilter.autoRemove = (val) => !val || (!val[0] && !val[1]);
// Multi-value filter (checkbox group)
const multiValueFilter: FilterFn<any> = (row, columnId, filterValue) => {
const values = filterValue as string[];
if (!values.length) return true;
return values.includes(row.getValue(columnId));
};
multiValueFilter.autoRemove = (val) => !val || val.length === 0;
// Register and use
const columns = [
columnHelper.accessor('createdAt', {
filterFn: dateRangeFilter,
}),
columnHelper.accessor('role', {
filterFn: multiValueFilter,
}),
];Fix 5: Virtualized Rows for Large Datasets
For tables with thousands of rows, use @tanstack/react-virtual:
npm install @tanstack/react-virtualimport { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';
function VirtualTable({ data }: { data: User[] }) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
});
const { rows } = table.getRowModel();
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: rows.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 40, // Row height in px
overscan: 10, // Render extra rows above/below viewport
});
return (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
<table style={{ width: '100%' }}>
<thead>
{table.getHeaderGroups().map(hg => (
<tr key={hg.id}>
{hg.headers.map(h => (
<th key={h.id} onClick={h.column.getToggleSortingHandler()}>
{flexRender(h.column.columnDef.header, h.getContext())}
</th>
))}
</tr>
))}
</thead>
<tbody
style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}
>
{virtualizer.getVirtualItems().map(virtualRow => {
const row = rows[virtualRow.index];
return (
<tr
key={row.id}
style={{
position: 'absolute',
top: 0,
transform: `translateY(${virtualRow.start}px)`,
height: `${virtualRow.size}px`,
}}
>
{row.getVisibleCells().map(cell => (
<td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
);
})}
</tbody>
</table>
</div>
);
}Fix 6: Migrate from v7 to v8
// v7 → v8 key changes
// 1. useTable() → useReactTable()
// v7:
import { useTable, useSortBy, useFilters } from 'react-table';
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } =
useTable({ columns, data }, useFilters, useSortBy);
// v8:
import { useReactTable, getCoreRowModel, getSortedRowModel } from '@tanstack/react-table';
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
state: { sorting },
onSortingChange: setSorting,
});
// 2. Column definitions changed
// v7:
const columns = [{ accessor: 'name', Header: 'Name' }];
// v8:
const columnHelper = createColumnHelper<User>();
const columns = [columnHelper.accessor('name', { header: 'Name' })];
// 3. Rendering changed
// v7:
rows.map(row => {
prepareRow(row);
return <tr {...row.getRowProps()}>...</tr>;
});
// v8:
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>
));
// 4. Props spread → explicit props
// v7: {...column.getHeaderProps(column.getSortByToggleProps())}
// v8: onClick={header.column.getToggleSortingHandler()}Still Not Working?
Sorting arrow shows but data order doesn’t change — you likely registered getSortedRowModel() but forgot to add sorting to state and onSortingChange to the table config. Without both, the sort state is internal but doesn’t drive rendering. All three pieces are required: the row model, the state slice, and the change handler.
Global filter doesn’t filter on all columns — getFilteredRowModel with globalFilter only searches columns where enableGlobalFilter is not false. By default it’s enabled. If filtering still doesn’t work, check that getGlobalAutoFilterFn is appropriate for your data types. For custom filter logic, provide globalFilterFn to the table config.
flexRender returns undefined or crashes — this happens when the cell definition is undefined in a column. Every column must have a cell definition (or use the default accessor which renders the value as a string). For display columns (columnHelper.display), always provide a cell function explicitly.
For related data display issues, see Fix: React useState Not Updating and Fix: TanStack Query Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: AutoAnimate Not Working — Transitions Not Playing, List Items Not Animating, or React State Changes Ignored
How to fix @formkit/auto-animate issues — parent ref setup, React useAutoAnimate hook, Vue directive, animation customization, disabling for specific elements, and framework integration.
Fix: Blurhash Not Working — Placeholder Not Rendering, Encoding Failing, or Colors Wrong
How to fix Blurhash image placeholder issues — encoding with Sharp, decoding in React, canvas rendering, Next.js image placeholders, CSS blur fallback, and performance optimization.
Fix: Embla Carousel Not Working — Slides Not Scrolling, Autoplay Not Starting, or Thumbnails Not Syncing
How to fix Embla Carousel issues — React setup, slide sizing, autoplay and navigation plugins, loop mode, thumbnail carousels, responsive breakpoints, and vertical scrolling.
Fix: i18next Not Working — Translations Missing, Language Not Switching, or Namespace Errors
How to fix i18next issues — react-i18next setup, translation file loading, namespace configuration, language detection, interpolation, pluralization, and Next.js integration.