Skip to content

Fix: TanStack Table Not Working — Sorting Not Triggering, Filters Ignored, or Pagination Showing Wrong Data

FixDevs ·

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-render

Or 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 unchanged

Or 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 1

Or after upgrading from v7 to v8, types break everywhere:

// v7
const { rows } = useTable({ columns, data });
// Error: Property 'useTable' does not exist

Why This Happens

TanStack Table v8 requires explicit opt-in for every feature:

  • Row models must be explicitly registeredgetCoreRowModel() is the only required model. Sorting, filtering, and pagination each need their own model (getSortedRowModel, getFilteredRowModel, getPaginationRowModel) plus the corresponding state and event handler.
  • state must be controlled — features like sorting require you to declare both the state object and the on*Change handler. Declaring one without the other leaves the feature in a broken half-controlled state.
  • v8 is a complete rewriteuseTable, useSortBy, useFilters, etc. are gone. Everything is now in a single useReactTable hook with a composable API. Column definitions use columnHelper and 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-virtual
import { 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 columnsgetFilteredRowModel 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles