Introduction to Interactive Table Features

Welcome back, future TanStack wizard! In the previous chapter, we laid the groundwork for building a basic table using TanStack Table. We learned how to define columns, provide data, and render a static grid of information. But let’s be honest, static tables are rarely enough in real-world applications. Users expect to interact with their data: to find specific entries, sort by relevance, and navigate through large datasets without being overwhelmed.

This chapter is all about bringing your tables to life! We’ll dive deep into three fundamental interactive features: sorting, filtering, and pagination. These features are crucial for enhancing user experience, making large datasets manageable, and transforming your tables from mere displays into powerful data exploration tools. By the end of this chapter, you’ll be able to implement these features efficiently, building on the headless principles of TanStack Table.

Before we begin, ensure you’re comfortable with the basic TanStack Table setup from the previous chapter. We’ll be extending that foundation, so having a working basic table component is a great starting point!

Core Concepts: Making Tables Dynamic

TanStack Table, being a “headless” library, provides you with powerful hooks and utilities to manage table state and logic, leaving the UI rendering entirely up to you. This means we’ll interact with its API to control sorting, filtering, and pagination, then use that controlled state to render our table components accordingly.

Let’s break down each concept:

1. Sorting Data

Sorting allows users to reorder table rows based on the values in one or more columns. Imagine a list of products; users might want to sort them by price (ascending or descending) or by name (alphabetically).

What it is: Arranging data in a specific order (e.g., alphabetical, numerical, chronological). Why it’s important: Helps users quickly find information, compare values, and understand trends in data. How TanStack Table handles it: TanStack Table manages sorting state (which columns are sorted and in what direction) and provides functions to sort your data. Key concepts include:

  • enableSorting: A column definition option to enable sorting for a specific column.
  • sorting: A state variable that holds an array of objects, each describing a sorted column ({ id: 'columnId', desc: true/false }).
  • onSortingChange: A function to update the sorting state when a user interacts with a column header.
  • getSortedRowModel(): A “row model” accessor that tells TanStack Table to process rows based on the current sorting state.

2. Filtering Data

Filtering lets users narrow down the displayed data to only show rows that match specific criteria. For instance, filtering a product list to only show items “in stock” or “under $50”.

What it is: Displaying a subset of data that meets certain conditions. Why it’s important: Reduces visual clutter, helps users focus on relevant information, and improves data discoverability. How TanStack Table handles it: TanStack Table supports two main types of filtering:

  • Global Filter: A single input field that searches across all filterable columns. Think of it as a general search bar for your table.
  • Column Filters: Specific input fields for each column, allowing fine-grained filtering on individual data properties.

Key concepts for filtering:

  • enableColumnFilter: A column definition option to enable filtering for a specific column.
  • globalFilter: A state variable (usually a string) that holds the current global search term.
  • columnFilters: A state variable (an array of objects) that holds the current individual column filter terms.
  • onGlobalFilterChange / onColumnFiltersChange: Functions to update the respective filter states.
  • getFilteredRowModel(): A “row model” accessor that processes rows based on globalFilter and columnFilters states.
  • filterFns: You can define custom filter functions for specific columns or globally.

3. Pagination Data

Pagination breaks a large dataset into smaller, more manageable “pages.” Instead of loading thousands of rows at once, users can view data in chunks, navigating between pages.

What it is: Dividing a large list of items into discrete pages. Why it’s important: Improves performance by rendering fewer rows at a time, reduces initial load times, and enhances user experience by preventing overwhelming scrollable lists. How TanStack Table handles it:

  • pagination: A state variable that holds the current page index and page size ({ pageIndex: 0, pageSize: 10 }).
  • onPaginationChange: A function to update the pagination state.
  • getPaginationRowModel(): A “row model” accessor that processes rows based on the pagination state, only showing the rows for the current page.
  • pageCount: The total number of pages available.

A Mental Model: The Table Pipeline

Think of TanStack Table as a data processing pipeline. Your raw data goes in, and then it flows through various “row models” that transform it.

graph TD A["Raw Data"] --> B("getCoreRowModel()") B --> C("getSortedRowModel()") C --> D("getFilteredRowModel()") D --> E("getPaginationRowModel()") E --> F["Rendered Rows"] subgraph User Interaction ClickSort["Click Header"] --> C TypeFilter["Type in Filter"] --> D ClickPage["Click Page Button"] --> E end

In this diagram:

  • Raw Data is your initial array of objects.
  • getCoreRowModel() is always the first step, creating the initial row objects.
  • getSortedRowModel() takes the rows and sorts them based on the sorting state.
  • getFilteredRowModel() then takes the sorted rows and filters them based on globalFilter and columnFilters.
  • getPaginationRowModel() takes the sorted and filtered rows and extracts only the ones for the current page.
  • Finally, Rendered Rows are what you display in your UI.

This pipeline ensures that operations happen in a logical order: first sort, then filter, then paginate.

Step-by-Step Implementation

Let’s build a fully interactive table! We’ll use React for our example, but the core TanStack Table logic applies across frameworks.

Prerequisites: Make sure you have react and @tanstack/react-table installed. As of January 2026, the latest stable version of @tanstack/react-table is v8.11.3 (or newer patch versions).

npm install @tanstack/react-table@latest react react-dom
# or
yarn add @tanstack/react-table@latest react react-dom

We’ll start with a basic table structure. Create a file named InteractiveTable.jsx (or .tsx if you’re using TypeScript).

// InteractiveTable.jsx
import React from 'react';
import {
  useReactTable,
  getCoreRowModel,
  flexRender,
} from '@tanstack/react-table';

const defaultData = [
  { id: 1, firstName: 'Alice', lastName: 'Smith', age: 30, city: 'New York' },
  { id: 2, firstName: 'Bob', lastName: 'Johnson', age: 24, city: 'Los Angeles' },
  { id: 3, firstName: 'Charlie', lastName: 'Brown', age: 35, city: 'Chicago' },
  { id: 4, firstName: 'Diana', lastName: 'Prince', age: 28, city: 'Miami' },
  { id: 5, firstName: 'Eve', lastName: 'Adams', age: 42, city: 'New York' },
  { id: 6, firstName: 'Frank', lastName: 'Miller', age: 29, city: 'Chicago' },
  { id: 7, firstName: 'Grace', lastName: 'Davis', age: 31, city: 'Los Angeles' },
  { id: 8, firstName: 'Heidi', lastName: 'White', age: 22, city: 'Miami' },
  { id: 9, firstName: 'Ivan', lastName: 'Black', age: 38, city: 'New York' },
  { id: 10, firstName: 'Judy', lastName: 'Green', age: 27, city: 'Chicago' },
];

const InteractiveTable = () => {
  // 1. Define Columns
  const columns = React.useMemo(
    () => [
      {
        accessorKey: 'firstName',
        header: 'First Name',
      },
      {
        accessorKey: 'lastName',
        header: 'Last Name',
      },
      {
        accessorKey: 'age',
        header: 'Age',
      },
      {
        accessorKey: 'city',
        header: 'City',
      },
    ],
    []
  );

  // 2. Initialize table state and core logic
  const table = useReactTable({
    data: defaultData,
    columns,
    getCoreRowModel: getCoreRowModel(),
  });

  return (
    <div className="p-4">
      <h2 className="text-2xl font-bold mb-4">Interactive User Data</h2>
      <table className="min-w-full divide-y divide-gray-200 border border-gray-300">
        <thead className="bg-gray-50">
          {table.getHeaderGroups().map(headerGroup => (
            <tr key={headerGroup.id}>
              {headerGroup.headers.map(header => (
                <th
                  key={header.id}
                  className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
                >
                  {header.isPlaceholder
                    ? null
                    : flexRender(
                        header.column.columnDef.header,
                        header.getContext()
                      )}
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody className="bg-white divide-y divide-gray-200">
          {table.getRowModel().rows.map(row => (
            <tr key={row.id}>
              {row.getVisibleCells().map(cell => (
                <td
                  key={cell.id}
                  className="px-6 py-4 whitespace-nowrap text-sm text-gray-900"
                >
                  {flexRender(cell.column.columnDef.cell, cell.getContext())}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
};

export default InteractiveTable;

This is our baseline. Now, let’s add the features!

Step 1: Add Sorting

To enable sorting, we need to:

  1. Manage a sorting state.
  2. Pass onSortingChange and getSortedRowModel to useReactTable.
  3. Update column headers to display sorting direction and handle clicks.
// ... (imports and defaultData remain the same)
import {
  useReactTable,
  getCoreRowModel,
  getSortedRowModel, // <-- New import!
  flexRender,
} from '@tanstack/react-table';

const InteractiveTable = () => {
  // ... (columns definition remains the same)

  // 1. Add sorting state
  const [sorting, setSorting] = React.useState([]); // <-- New state!

  // 2. Update useReactTable options
  const table = useReactTable({
    data: defaultData,
    columns,
    getCoreRowModel: getCoreRowModel(),
    // Add sorting options
    onSortingChange: setSorting, // <-- New option!
    getSortedRowModel: getSortedRowModel(), // <-- New option!
    state: {
      sorting, // <-- Connect state!
    },
  });

  return (
    <div className="p-4">
      <h2 className="text-2xl font-bold mb-4">Interactive User Data</h2>
      <table className="min-w-full divide-y divide-gray-200 border border-gray-300">
        <thead className="bg-gray-50">
          {table.getHeaderGroups().map(headerGroup => (
            <tr key={headerGroup.id}>
              {headerGroup.headers.map(header => (
                <th
                  key={header.id}
                  className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer select-none" // <-- Added cursor-pointer
                  onClick={header.column.getToggleSortingHandler()} // <-- New click handler!
                >
                  {header.isPlaceholder
                    ? null
                    : (
                        <div className="flex items-center gap-1">
                          {flexRender(
                            header.column.columnDef.header,
                            header.getContext()
                          )}
                          {/* Display sorting indicator */}
                          {{
                            asc: ' ๐Ÿ”ผ', // <-- New indicator!
                            desc: ' ๐Ÿ”ฝ', // <-- New indicator!
                          }[header.column.getIsSorted()] ?? null}
                        </div>
                      )}
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody className="bg-white divide-y divide-gray-200">
          {table.getRowModel().rows.map(row => (
            <tr key={row.id}>
              {row.getVisibleCells().map(cell => (
                <td
                  key={cell.id}
                  className="px-6 py-4 whitespace-nowrap text-sm text-gray-900"
                >
                  {flexRender(cell.column.columnDef.cell, cell.getContext())}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
};

export default InteractiveTable;

Explanation of changes:

  • We import getSortedRowModel from @tanstack/react-table.
  • const [sorting, setSorting] = React.useState([]); declares a state variable sorting to hold the current sort order. It’s an array because you can sort by multiple columns (though we’re only implementing single-column sorting for now).
  • In useReactTable, we pass onSortingChange: setSorting to tell the table how to update our state when sorting changes.
  • getSortedRowModel: getSortedRowModel() activates the sorting logic in the pipeline.
  • state: { sorting, } connects our local sorting state to the table instance.
  • In the <th> element:
    • cursor-pointer select-none makes it look clickable.
    • onClick={header.column.getToggleSortingHandler()} is the magic! This function, provided by TanStack Table, toggles the sort direction for that column (none -> asc -> desc -> none).
    • We added a visual indicator: header.column.getIsSorted() returns 'asc', 'desc', or false. We use this to display an arrow.

Now, click on the table headers! You’ll see the data reorder and the arrows appear. Pretty neat, right?

Step 2: Add Global Filtering

Next, let’s add a global search input.

// ... (imports and defaultData remain the same)
import {
  useReactTable,
  getCoreRowModel,
  getSortedRowModel,
  getFilteredRowModel, // <-- New import!
  flexRender,
} from '@tanstack/react-table';

const InteractiveTable = () => {
  // ... (columns definition remains the same)

  const [sorting, setSorting] = React.useState([]);
  // 1. Add global filter state
  const [globalFilter, setGlobalFilter] = React.useState(''); // <-- New state!

  // 2. Update useReactTable options
  const table = useReactTable({
    data: defaultData,
    columns,
    getCoreRowModel: getCoreRowModel(),
    onSortingChange: setSorting,
    getSortedRowModel: getSortedRowModel(),
    // Add global filter options
    onGlobalFilterChange: setGlobalFilter, // <-- New option!
    getFilteredRowModel: getFilteredRowModel(), // <-- New option!
    state: {
      sorting,
      globalFilter, // <-- Connect state!
    },
  });

  return (
    <div className="p-4">
      <h2 className="text-2xl font-bold mb-4">Interactive User Data</h2>
      {/* Global Filter Input */}
      <input
        type="text"
        value={globalFilter ?? ''} // <-- New input!
        onChange={e => setGlobalFilter(e.target.value)}
        placeholder="Search all columns..."
        className="mb-4 p-2 border border-gray-300 rounded-md w-full max-w-xs"
      />
      <table className="min-w-full divide-y divide-gray-200 border border-gray-300">
        <thead className="bg-gray-50">
          {table.getHeaderGroups().map(headerGroup => (
            <tr key={headerGroup.id}>
              {headerGroup.headers.map(header => (
                <th
                  key={header.id}
                  className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer select-none"
                  onClick={header.column.getToggleSortingHandler()}
                >
                  {header.isPlaceholder
                    ? null
                    : (
                        <div className="flex items-center gap-1">
                          {flexRender(
                            header.column.columnDef.header,
                            header.getContext()
                          )}
                          {{
                            asc: ' ๐Ÿ”ผ',
                            desc: ' ๐Ÿ”ฝ',
                          }[header.column.getIsSorted()] ?? null}
                        </div>
                      )}
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody className="bg-white divide-y divide-gray-200">
          {table.getRowModel().rows.map(row => (
            <tr key={row.id}>
              {row.getVisibleCells().map(cell => (
                <td
                  key={cell.id}
                  className="px-6 py-4 whitespace-nowrap text-sm text-gray-900"
                >
                  {flexRender(cell.column.columnDef.cell, cell.getContext())}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
};

export default InteractiveTable;

Explanation of changes:

  • We import getFilteredRowModel.
  • const [globalFilter, setGlobalFilter] = React.useState(''); creates a state for our global search term.
  • onGlobalFilterChange: setGlobalFilter and getFilteredRowModel: getFilteredRowModel() are added to useReactTable to enable global filtering.
  • state: { globalFilter, } links our state.
  • A new <input> element is added above the table. Its value is controlled by globalFilter, and its onChange handler updates globalFilter via setGlobalFilter.

Type something into the search box, like “New York” or “Alice”. The table should instantly filter to show only matching rows!

Step 3: Add Column Filtering

Column filtering is slightly more involved as it requires an input for each column. We’ll add a helper component to manage this.

First, let’s create a small Filter component:

// Filter.jsx
import React from 'react';

function Filter({ column }) {
  const columnFilterValue = column.getFilterValue();

  return (
    <input
      type="text"
      value={(columnFilterValue ?? '')}
      onChange={e => column.setFilterValue(e.target.value)}
      placeholder={`Search ${column.columnDef.header}...`}
      className="w-36 border shadow rounded px-2 py-1 text-sm mt-1"
      onClick={e => e.stopPropagation()} // Prevent sort toggle when clicking filter input
    />
  );
}

export default Filter;

Now, integrate this into InteractiveTable.jsx:

// ... (imports and defaultData remain the same)
import {
  useReactTable,
  getCoreRowModel,
  getSortedRowModel,
  getFilteredRowModel,
  flexRender,
} from '@tanstack/react-table';
import Filter from './Filter'; // <-- New import!

const InteractiveTable = () => {
  // 1. Update columns to enable column filtering
  const columns = React.useMemo(
    () => [
      {
        accessorKey: 'firstName',
        header: 'First Name',
        enableColumnFilter: true, // <-- Enable column filter
      },
      {
        accessorKey: 'lastName',
        header: 'Last Name',
        enableColumnFilter: true, // <-- Enable column filter
      },
      {
        accessorKey: 'age',
        header: 'Age',
        enableColumnFilter: true, // <-- Enable column filter
      },
      {
        accessorKey: 'city',
        header: 'City',
        enableColumnFilter: true, // <-- Enable column filter
      },
    ],
    []
  );

  const [sorting, setSorting] = React.useState([]);
  const [globalFilter, setGlobalFilter] = React.useState('');
  // 2. Add column filter state
  const [columnFilters, setColumnFilters] = React.useState([]); // <-- New state!

  // 3. Update useReactTable options
  const table = useReactTable({
    data: defaultData,
    columns,
    getCoreRowModel: getCoreRowModel(),
    onSortingChange: setSorting,
    getSortedRowModel: getSortedRowModel(),
    onGlobalFilterChange: setGlobalFilter,
    getFilteredRowModel: getFilteredRowModel(),
    // Add column filter options
    onColumnFiltersChange: setColumnFilters, // <-- New option!
    state: {
      sorting,
      globalFilter,
      columnFilters, // <-- Connect state!
    },
  });

  return (
    <div className="p-4">
      <h2 className="text-2xl font-bold mb-4">Interactive User Data</h2>
      <input
        type="text"
        value={globalFilter ?? ''}
        onChange={e => setGlobalFilter(e.target.value)}
        placeholder="Search all columns..."
        className="mb-4 p-2 border border-gray-300 rounded-md w-full max-w-xs"
      />
      <table className="min-w-full divide-y divide-gray-200 border border-gray-300">
        <thead className="bg-gray-50">
          {table.getHeaderGroups().map(headerGroup => (
            <tr key={headerGroup.id}>
              {headerGroup.headers.map(header => (
                <th
                  key={header.id}
                  className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
                >
                  <div
                    className="flex items-center gap-1 cursor-pointer select-none"
                    onClick={header.column.getToggleSortingHandler()}
                  >
                    {header.isPlaceholder
                      ? null
                      : flexRender(
                          header.column.columnDef.header,
                          header.getContext()
                        )}
                    {{
                      asc: ' ๐Ÿ”ผ',
                      desc: ' ๐Ÿ”ฝ',
                    }[header.column.getIsSorted()] ?? null}
                  </div>
                  {/* Render column filter input if column is filterable */}
                  {header.column.getCanFilter() ? ( // <-- Check if filterable!
                    <Filter column={header.column} /> // <-- Render Filter component!
                  ) : null}
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody className="bg-white divide-y divide-gray-200">
          {table.getRowModel().rows.map(row => (
            <tr key={row.id}>
              {row.getVisibleCells().map(cell => (
                <td
                  key={cell.id}
                  className="px-6 py-4 whitespace-nowrap text-sm text-gray-900"
                >
                  {flexRender(cell.column.columnDef.cell, cell.getContext())}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
};

export default InteractiveTable;

Explanation of changes:

  • We import our new Filter component.
  • In the columns definition, enableColumnFilter: true is added to each column you want to be individually filterable.
  • const [columnFilters, setColumnFilters] = React.useState([]); creates state for column-specific filters.
  • onColumnFiltersChange: setColumnFilters and state: { columnFilters, } connect the column filter state to useReactTable.
  • Inside the <th> element, we wrap the flexRender and sorting indicator in a div so that the onClick for sorting only applies to the header text, not the filter input.
  • We then check header.column.getCanFilter() to see if the column is configured for filtering. If it is, we render our Filter component, passing the header.column object to it.
  • In Filter.jsx, column.getFilterValue() retrieves the current filter value for that column, and column.setFilterValue(e.target.value) updates it. e.stopPropagation() on the input’s onClick prevents the sort handler on the th from triggering when you click inside the filter input.

Now you have both global and per-column filtering! Try filtering by “New York” in the global search, then by “Alice” in the “First Name” column. Notice how they combine!

Step 4: Add Pagination

Finally, let’s add pagination controls.

// ... (imports and defaultData remain the same)
import {
  useReactTable,
  getCoreRowModel,
  getSortedRowModel,
  getFilteredRowModel,
  getPaginationRowModel, // <-- New import!
  flexRender,
} from '@tanstack/react-table';
import Filter from './Filter';

const InteractiveTable = () => {
  // ... (columns definition remains the same)

  const [sorting, setSorting] = React.useState([]);
  const [globalFilter, setGlobalFilter] = React.useState('');
  const [columnFilters, setColumnFilters] = React.useState([]);
  // 1. Add pagination state
  const [pagination, setPagination] = React.useState({
    pageIndex: 0, // Start on the first page (index 0)
    pageSize: 5,  // Show 5 items per page
  }); // <-- New state!

  // 2. Update useReactTable options
  const table = useReactTable({
    data: defaultData,
    columns,
    getCoreRowModel: getCoreRowModel(),
    onSortingChange: setSorting,
    getSortedRowModel: getSortedRowModel(),
    onGlobalFilterChange: setGlobalFilter,
    getFilteredRowModel: getFilteredRowModel(),
    // Add pagination options
    onPaginationChange: setPagination, // <-- New option!
    getPaginationRowModel: getPaginationRowModel(), // <-- New option!
    state: {
      sorting,
      globalFilter,
      columnFilters,
      pagination, // <-- Connect state!
    },
  });

  return (
    <div className="p-4">
      <h2 className="text-2xl font-bold mb-4">Interactive User Data</h2>
      <input
        type="text"
        value={globalFilter ?? ''}
        onChange={e => setGlobalFilter(e.target.value)}
        placeholder="Search all columns..."
        className="mb-4 p-2 border border-gray-300 rounded-md w-full max-w-xs"
      />
      <table className="min-w-full divide-y divide-gray-200 border border-gray-300">
        <thead className="bg-gray-50">
          {table.getHeaderGroups().map(headerGroup => (
            <tr key={headerGroup.id}>
              {headerGroup.headers.map(header => (
                <th
                  key={header.id}
                  className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
                >
                  <div
                    className="flex items-center gap-1 cursor-pointer select-none"
                    onClick={header.column.getToggleSortingHandler()}
                  >
                    {header.isPlaceholder
                      ? null
                      : flexRender(
                          header.column.columnDef.header,
                          header.getContext()
                        )}
                    {{
                      asc: ' ๐Ÿ”ผ',
                      desc: ' ๐Ÿ”ฝ',
                    }[header.column.getIsSorted()] ?? null}
                  </div>
                  {header.column.getCanFilter() ? (
                    <Filter column={header.column} />
                  ) : null}
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody className="bg-white divide-y divide-gray-200">
          {/* Render only the rows for the current page */}
          {table.getRowModel().rows.map(row => ( // <-- This already gives page rows
            <tr key={row.id}>
              {row.getVisibleCells().map(cell => (
                <td
                  key={cell.id}
                  className="px-6 py-4 whitespace-nowrap text-sm text-gray-900"
                >
                  {flexRender(cell.column.columnDef.cell, cell.getContext())}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>

      {/* Pagination Controls */}
      <div className="flex items-center gap-2 mt-4">
        <button
          onClick={() => table.previousPage()}
          disabled={!table.getCanPreviousPage()}
          className="px-3 py-1 border rounded-md disabled:opacity-50"
        >
          {'<'}
        </button>
        <button
          onClick={() => table.nextPage()}
          disabled={!table.getCanNextPage()}
          className="px-3 py-1 border rounded-md disabled:opacity-50"
        >
          {'>'}
        </button>
        <span className="flex items-center gap-1">
          Page
          <strong>
            {table.getState().pagination.pageIndex + 1} of{' '}
            {table.getPageCount()}
          </strong>
        </span>
        <span className="flex items-center gap-1">
          | Go to page:
          <input
            type="number"
            defaultValue={table.getState().pagination.pageIndex + 1}
            onChange={e => {
              const page = e.target.value ? Number(e.target.value) - 1 : 0;
              table.setPageIndex(page);
            }}
            className="border p-1 rounded w-16"
          />
        </span>
        <select
          value={table.getState().pagination.pageSize}
          onChange={e => {
            table.setPageSize(Number(e.target.value));
          }}
          className="border p-1 rounded"
        >
          {[5, 10, 20, 30, 40, 50].map(pageSize => (
            <option key={pageSize} value={pageSize}>
              Show {pageSize}
            </option>
          ))}
        </select>
      </div>
    </div>
  );
};

export default InteractiveTable;

Explanation of changes:

  • We import getPaginationRowModel.
  • const [pagination, setPagination] = React.useState({ pageIndex: 0, pageSize: 5 }); initializes our pagination state. We start on the first page (index 0) and show 5 items per page.
  • onPaginationChange: setPagination and getPaginationRowModel: getPaginationRowModel() are added to useReactTable.
  • state: { pagination, } links our state.
  • Crucially, table.getRowModel().rows already returns the rows for the current page because getPaginationRowModel() has processed them. No changes are needed in the <tbody> rendering loop itself!
  • We add a new div for pagination controls:
    • table.previousPage() and table.nextPage() are functions to change the page.
    • table.getCanPreviousPage() and table.getCanNextPage() tell us if there are pages before/after the current one, used to disable buttons.
    • table.getState().pagination.pageIndex + 1 gives the current page number (human-readable).
    • table.getPageCount() gives the total number of pages.
    • table.setPageIndex() and table.setPageSize() allow programmatic control over page index and size.
    • A <select> element lets users change the number of rows per page.

Now you have a fully interactive table with sorting, global filtering, column filtering, and pagination! Try playing with all the controls. Notice how filtering and sorting apply before pagination, ensuring you paginate through the relevant subset of data.

Mini-Challenge: Custom Filter for Age Range

Challenge: Instead of a simple text input for the ‘Age’ column, create a custom filter that allows users to filter by an age range (e.g., “show users between 25 and 35”). You’ll need two input fields (min age, max age) for this.

Hint:

  1. You can define a custom filterFn property in the column definition for ‘age’.
  2. Your Filter component for the ‘age’ column will need to render two inputs and update the filter value as an object or array (e.g., { min: 25, max: 35 }).
  3. The filterFn will receive the row, columnId, and filterValue (your range object/array) and should return true if the row should be included, false otherwise.

What to observe/learn: This challenge will teach you how to extend TanStack Table’s filtering capabilities beyond simple string matching, demonstrating its flexibility with custom logic.

Common Pitfalls & Troubleshooting

  1. Forgetting Row Models: The most common mistake is forgetting to include getSortedRowModel(), getFilteredRowModel(), or getPaginationRowModel() in your useReactTable options. If your data isn’t sorting/filtering/paginating, double-check these are present.
  2. State Mismatch: Ensure your local state variables (sorting, globalFilter, columnFilters, pagination) are correctly connected to the useReactTable instance via the state property and updated via onSortingChange, onGlobalFilterChange, etc. If the UI updates but the table doesn’t react, or vice-versa, check this connection.
  3. Incorrect accessorKey: If a column doesn’t sort or filter as expected, verify that its accessorKey exactly matches the property name in your data objects.
  4. Performance with Large Datasets (Client-Side): While TanStack Table is highly optimized, client-side sorting, filtering, and pagination on extremely large datasets (tens of thousands of rows or more) can still lead to performance issues. If you encounter sluggishness, it’s a strong indicator that you should consider server-side processing for these features. We’ll touch upon this in a later chapter, often integrating with TanStack Query.
  5. onClick Propagation: When adding filter inputs inside a sortable header, remember to e.stopPropagation() on the input’s onClick or onChange events to prevent the header’s sort toggle from firing simultaneously.

Summary

Congratulations! You’ve successfully transformed a static table into a dynamic, user-friendly data exploration tool. Here’s a quick recap of what we covered:

  • Sorting: How to enable sorting, manage sorting state, use onSortingChange, and integrate getSortedRowModel() to reorder your data.
  • Global Filtering: Implementing a single search input that filters across all columns using globalFilter, onGlobalFilterChange, and getFilteredRowModel().
  • Column Filtering: Adding individual filter inputs for specific columns, managing columnFilters state, and connecting with onColumnFiltersChange.
  • Pagination: Breaking down large datasets into manageable pages using pagination state, onPaginationChange, and getPaginationRowModel(), along with UI controls for navigation and page size.
  • The Table Pipeline: Understanding how TanStack Table processes your data through a series of row models (getCoreRowModel -> getSortedRowModel -> getFilteredRowModel -> getPaginationRowModel).
  • Common Pitfalls: Identifying and troubleshooting issues related to missing row models, state mismatches, and performance considerations.

These interactive features are fundamental to almost any data-driven application. In the next chapter, we’ll delve deeper into TanStack Table’s capabilities, exploring advanced column features, row selection, and potentially more complex data transformations.

References


This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.