Server-Side Pagination in Ant Design Table: Doing It Right
Client-side pagination in Ant Design Table collapses under large datasets. Here's how to wire the pagination, onChange, and loading props to your API so the server does the slicing.
07 Apr 2026, 19:57 UTC

Your Ant Design Table works beautifully in development with 50 rows. Then production hands it 200,000 records, and the browser tab freezes while React tries to render every single one. The fix is not a bigger browser — it is moving pagination to the server, and AntD's Table supports this cleanly once you understand which props you now own.
The key mental shift: by default, Table paginates client-side — it slices the dataSource array for you. The moment your dataset is too big to ship to the browser, you take over that slicing yourself, and the pagination bar becomes a remote control for your API rather than a view over local data.
The three props that matter
Server-side pagination in AntD Table comes down to three things working together:
paginationas an object — passcurrent,pageSize, and cruciallytotal. Thetotalvalue is what lets the component render the correct number of page buttons even though it only ever sees one page of rows. Without an accurate total from your backend, the pager will under- or over-render pages.onChange— fires when the user clicks a page, changes page size, sorts, or filters. This is where you fire your next API request. Its signature isonChange(pagination, filters, sorter).loading— flip this totruewhile the request is in flight so users get a spinner over the table body instead of a stale or empty grid.
A worked example
Here is a minimal but complete pattern (React with hooks, AntD v5):
import { useEffect, useState } from 'react';
import { Table } from 'antd';
function UserTable() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [pagination, setPagination] = useState({
current: 1,
pageSize: 20,
total: 0,
showSizeChanger: true,
pageSizeOptions: ['10', '20', '50'],
});
const fetchPage = async (page, pageSize) => {
setLoading(true);
try {
const res = await fetch(
`/api/users?page=${page}&pageSize=${pageSize}`
);
const json = await res.json(); // { items: [...], total: 183422 }
setData(json.items);
setPagination((p) => ({ ...p, current: page, pageSize, total: json.total }));
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchPage(pagination.current, pagination.pageSize);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Table
rowKey="id"
columns={columns}
dataSource={data}
loading={loading}
pagination={pagination}
onChange={(pager) => fetchPage(pager.current, pager.pageSize)}
/>
);
}
Two details worth noting. First, dataSource only ever holds the current page's slice — never concatenate pages into it, or you reintroduce the memory problem you were solving. Second, the backend contract must return both the items and the total count; a common shape is { items: [...], total: number }.
Where this goes wrong
The most frequent bug is a stale total. If records are added or deleted between page fetches, page counts drift. Decide with your backend team whether total is re-queried per request (accurate, slightly costlier) or cached (faster, occasionally wrong). For most admin UIs, re-querying per page is fine.
Another trap: onChange also fires on sorter and filter changes. If you add server-side sorting later, reset current to 1 when filters change — otherwise users land on page 7 of a filtered result set that only has 2 pages, and see an empty table.
Finally, a limitation to be honest about: page-jump UX. With server-side pagination, jumping from page 1 to page 500 still costs a query with a large OFFSET, which gets slow on very big tables at the database level. If users routinely need deep pages, consider cursor-based pagination instead — though AntD's built-in pager UI is page-number oriented, so you'd be trading away the stock pagination bar.
Verify it before shipping
Three quick checks: (1) hardcode total: 1000 with 20 rows of data and confirm the pager renders 50 pages; (2) throttle your network in DevTools and confirm the loading spinner appears during fetches; (3) change the page size with showSizeChanger enabled and confirm your API receives the new pageSize and the table re-renders correctly. If all three pass, your pagination is genuinely server-driven — and your 200,000-row table will feel as light as the 50-row one did.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.