How to Handle Table Pagination, Sorting, and Filtering Effectively in Ant Design

Ant Design's Table component leverages three specialized React hooks—usePagination, useFilterSorter, and useFilter—to manage pagination, sorting, and filtering state, supporting both controlled and uncontrolled patterns while automatically injecting UI elements like dropdowns and sorter icons into column headers.

The Table component in the ant-design/ant-design repository provides a comprehensive data-grid solution that handles complex interactions through a modular hook-based architecture. Understanding how to effectively handle table pagination, sorting, and filtering in Ant Design requires familiarity with the internal hooks that power these features and the distinction between controlled and uncontrolled state management patterns.

Understanding the Core Architecture

Ant Design separates table concerns into three dedicated hooks that operate inside InternalTable (components/table/InternalTable.tsx). The public Table component (components/table/Table.tsx) serves as a thin wrapper that forwards refs and props to this internal implementation.

Feature Hook Location Primary Responsibility
Pagination components/table/hooks/usePagination.ts Merges user-provided pagination props with internal state, clamps page ranges, and exposes refreshPagination.
Sorting components/table/hooks/useSorter.tsx Collects sorter definitions from columns, tracks multi-column sort priority, and injects sorter UI into headers.
Filtering components/table/hooks/useFilter/index.tsx Manages filter states, renders filter dropdowns, and applies onFilter logic to data rows.

These hooks communicate through the onChange callback, which receives the current pagination, filters, and sorter state whenever user interaction occurs.

Pagination with usePagination

How usePagination Works

The usePagination hook initializes with DEFAULT_PAGE_SIZE = 10 and creates an innerPagination state object to track current and pageSize. It uses extendsObject to merge three sources of truth: internal state, user-provided pagination props, and the total data count.

When the data length changes (e.g., after filtering), the hook performs a page-range sanity check. If mergedPagination.current exceeds Math.ceil(total / pageSize), it clamps the value to the maximum valid page or resets to 1. This prevents empty page states when rows are deleted or filtered.

Controlled vs. Uncontrolled Pagination

Uncontrolled usage requires only a configuration object or true:

<Table
  columns={columns}
  dataSource={data}
  pagination={{ pageSize: 5, showSizeChanger: true }}
/>

The hook manages current internally via onInternalChange, which updates state and triggers the table-level onChange callback.

Controlled usage requires explicit state management:

const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);

<Table
  pagination={{
    current: page,
    pageSize,
    onChange: (p, s) => { setPage(p); setPageSize(s); },
  }}
/>

When pagination includes current and onChange, usePagination treats the configuration as controlled, respecting external state while still providing default fallbacks for unspecified properties.

Refreshing Pagination Programmatically

The hook exposes refreshPagination as the second return value. Call this function after data mutations to reset to page 1 or recalculate total pages:

const [paginationConfig, refreshPagination] = usePagination(
  total,
  onTableChange,
  paginationProps
);

// After deleting a row
refreshPagination(true); // resets to page 1

Sorting with useFilterSorter

Collecting Sort States

The useFilterSorter hook (exported from useSorter.tsx) traverses the column tree via collectSortStates(columns, init). It identifies sortable columns by checking for sorter properties:

  • Controlled: Column provides sortOrder ('ascend', 'descend', or false)
  • Uncontrolled: Column provides defaultSortOrder used during initialization (init === true)

The hook maintains sortStates as an array of SortState objects, each containing the column key, sort order, and priority information.

Multi-Column Sort Priority

Ant Design supports multi-column sorting through the multiple property in the sorter configuration:

{
  title: 'Name',
  dataIndex: 'name',
  sorter: {
    compare: (a, b) => a.name.localeCompare(b.name),
    multiple: 1, // Higher priority (primary sort)
  },
}

The hook uses getMultiplePriority to extract priority values. When triggerSorter fires, it sorts the sorterStates array by multiplePriority (descending) before calling generateSorterInfo, ensuring higher-priority columns sort first regardless of click order.

Injecting Sorter UI

The injectSorter function transforms column titles to include sort indicators:

  1. Direction calculation: nextSortDirection cycles through sortDirections (default ['ascend', 'descend'])
  2. Icon rendering: Displays CaretUpOutlined and CaretDownOutlined with active states
  3. Accessibility: Adds aria-sort, aria-label, and aria-description attributes
  4. Tooltips: Wraps sorter buttons in <Tooltip> when showSorterTooltip is enabled

The hook returns transformedColumns with injected sorter props and getSorters to retrieve current sort state.

Filtering with useFilter

Filter State Collection

The useFilter hook (located in components/table/hooks/useFilter/index.tsx) initializes filter states via collectFilterStates(columns, init):

  • Controlled: Column provides filteredValue (array of selected keys)
  • Uncontrolled: Uses defaultFilteredValue during initialization
  • Force filtered: Column filtered flag indicates active filter state for UI styling

The hook maintains filterStates containing column keys, filtered values, and filter functions.

Custom Filter Dropdowns

The injectFilter function replaces column titles with FilterDropdown components. For custom UIs, columns provide filterDropdown:

{
  title: 'Address',
  dataIndex: 'address',
  filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters }) => (
    <div style={{ padding: 8 }}>
      <input
        placeholder="Search address"
        value={selectedKeys[0] || ''}
        onChange={e => setSelectedKeys(e.target.value ? [e.target.value] : [])}
        onPressEnter={confirm}
      />
      <button onClick={confirm}>Search</button>
      <button onClick={clearFilters}>Reset</button>
    </div>
  ),
  onFilter: (value, record) => record.address.toLowerCase().includes(value.toLowerCase()),
}

The hook handles filterMultiple (checkbox vs. radio behavior), filterSearch (search within filter list), and filterIcon customization.

Applying Filters to Data

The getFilterData function recursively filters records:

if (column.onFilter && filteredKeys?.length) {
  data = data.filter(record => 
    filteredKeys.some(key => column.onFilter(key, record))
  );
}

For tree tables, the function recurses into child arrays while maintaining the same filter states. The hook returns filteredData and filters (the filter object passed to onChange).

Practical Implementation Examples

Basic Uncontrolled Table

For simple use cases, let Ant Design manage all state internally:

import React from 'react';
import { Table } from 'antd';

const data = [
  { key: 1, name: 'John', age: 32, address: 'NY' },
  { key: 2, name: 'Jane', age: 28, address: 'LA' },
  // …
];

const columns = [
  {
    title: 'Name',
    dataIndex: 'name',
    sorter: true,
    filterDropdown: true,
    filters: [
      { text: 'John', value: 'John' },
      { text: 'Jane', value: 'Jane' },
    ],
  },
  {
    title: 'Age',
    dataIndex: 'age',
    sorter: (a, b) => a.age - b.age,
    defaultSortOrder: 'descend',
  },
];

export default () => (
  <Table
    columns={columns}
    dataSource={data}
    pagination={{ pageSize: 5 }}
  />
);

Clicking the Age header toggles between ascending and descending order, while the Name column provides a checkbox filter dropdown. Pagination automatically manages page numbers and size.

Controlled Pagination

When you need external control over pagination state:

import React, { useState } from 'react';
import { Table, Pagination } from 'antd';

const App = () => {
  const [page, setPage] = useState(1);
  const [pageSize, setPageSize] = useState(10);

  const pagination = {
    current: page,
    pageSize,
    total: 124,
    onChange: (p) => setPage(p),
    showSizeChanger: true,
    onShowSizeChange: (_, size) => setPageSize(size),
  };

  return (
    <>
      <Table
        columns={columns}
        dataSource={data}
        pagination={pagination}
      />
      <Pagination {...pagination} style={{ marginTop: 16 }} />
    </>
  );
};

Because pagination includes current and onChange, usePagination treats the configuration as controlled, respecting your external state while still providing default fallbacks for unspecified properties.

Multi-Column Sorting

Enable sorting across multiple columns simultaneously using priority values:

const multiColumns = [
  {
    title: 'Name',
    dataIndex: 'name',
    sorter: { 
      compare: (a, b) => a.name.localeCompare(b.name), 
      multiple: 1 
    },
  },
  {
    title: 'Age',
    dataIndex: 'age',
    sorter: { 
      compare: (a, b) => a.age - b.age, 
      multiple: 2 
    },
  },
];

<Table columns={multiColumns} dataSource={data} />

The multiple property defines sort priority (1 is primary, 2 is secondary). When you click the Age header after Name, the table sorts by name first, then by age within matching names.

Controlled Filtering with Custom UI

Implement custom filter interfaces while maintaining controlled state:

import { Table } from 'antd';
import type { FilterValue, TableColumnType } from 'antd/es/table/interface';

const [filters, setFilters] = React.useState<Record<string, FilterValue | null>>({});

const columns: TableColumnType<any>[] = [
  {
    title: 'Address',
    dataIndex: 'address',
    filterDropdown: ({ setSelectedKeys, selectedKeys, confirm }) => (
      <div style={{ padding: 8 }}>
        <input
          placeholder="Search address"
          value={selectedKeys[0] || ''}
          onChange={e => setSelectedKeys(e.target.value ? [e.target.value] : [])}
          onPressEnter={confirm}
          style={{ width: 188, marginBottom: 8, display: 'block' }}
        />
        <button onClick={confirm}>Search</button>
      </div>
    ),
    filteredValue: filters.address ? [filters.address] : null,
    onFilter: (value, record) =>
      record.address.toString().toLowerCase().includes((value as string).toLowerCase()),
  },
];

<Table
  columns={columns}
  dataSource={data}
  onChange={(_, __, ___, extra) => {
    setFilters(extra.currentFilters);
  }}
/>;

The column supplies filteredValue for controlled state and a custom filterDropdown for UI. The onChange handler receives extra.currentFilters, which you store in component state and feed back to the column.

Key Source Files Reference

Understanding the internal implementation helps debug complex table behaviors. The core logic resides in these locations within the ant-design/ant-design repository:

Feature File Path Purpose
Public API components/table/Table.tsx Wrapper component that forwards refs to InternalTable.
Core Logic components/table/InternalTable.tsx Orchestrates usePagination, useFilterSorter, and useFilter; renders the underlying RcTable.
Pagination components/table/hooks/usePagination.ts Manages current and pageSize state, handles page clamping, and exposes refreshPagination.
Sorting components/table/hooks/useSorter.tsx Implements collectSortStates, injectSorter, and triggerSorter for multi-column sort priority.
Filtering components/table/hooks/useFilter/index.tsx Handles collectFilterStates, injectFilter, and getFilterData for dropdown UI and data filtering.
Utilities components/table/util.ts Helper functions like getColumnKey and renderColumnTitle.
Base Table components/table/RcTable/index.tsx Low-level table implementation from rc-table that Ant Design extends.

Summary

  • Leverage built-in hooks instead of recreating logic. usePagination, useFilterSorter, and useFilter handle controlled/uncontrolled patterns, multi-column priority, and accessibility automatically.

  • Choose your control level based on application needs. Use uncontrolled mode for simple tables, or pass explicit current, pageSize, sortOrder, and filteredValue props for full external state management.

  • Compose features declaratively by adding sorter, filters, or filterDropdown to column definitions. The hooks automatically inject necessary UI elements and event handlers.

  • Maintain data immutability when applying sorts and filters. The hooks clone data arrays before transformation, preserving original references for React's change detection.

  • Reference source files like components/table/hooks/usePagination.ts and components/table/InternalTable.tsx when debugging edge cases or extending functionality.

Frequently Asked Questions

How do I reset pagination when data changes in Ant Design Table?

Call the refreshPagination function exposed by the usePagination hook, or simply update the pagination.current prop if using controlled mode. In components/table/hooks/usePagination.ts, refreshPagination recalculates the valid page range against the new total and clamps current to prevent empty pages.

Can I use server-side sorting and filtering with Ant Design Table?

Yes. Set pagination={{ current, pageSize }} and omit the sorter and onFilter functions from columns, instead using the onChange callback to trigger API requests. When onChange fires, it provides the current pagination, filters, and sorter objects, allowing you to fetch fresh data from your backend and feed it back into dataSource.

How do I implement custom filter icons in Ant Design Table?

Use the filterIcon column property to render custom React nodes. The useFilter hook in components/table/hooks/useFilter/index.tsx passes the filtered boolean to your icon component, allowing you to change the icon based on active filter state. For example: filterIcon: (filtered) => <SearchOutlined style={{ color: filtered ? '#1890ff' : undefined }} />.

What is the difference between controlled and uncontrolled filtering in Ant Design Table?

In uncontrolled mode, the useFilter hook manages filterStates internally using defaultFilteredValue from columns, and updates UI when users interact with filter dropdowns. In controlled mode, you provide filteredValue arrays to columns and manage the filter state externally, updating it via the onChange callback's filters parameter. Controlled mode is necessary when you need to persist filter state in URL query parameters or synchronize multiple table instances.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →