Pagination
Page size, page controls
Pagination splits your data into pages with a control bar at the bottom of the grid. It is off by default — the grid virtualizes (smoothly scrolls) all rows instead. Turn pagination on when you prefer page-by-page navigation.
Pagination vs. virtualization: EliteGrid can render a million rows smoothly without pagination thanks to virtualization (see Chapter 14). Pagination is a UX choice, not a performance requirement.
Jargon check — "page" and "page size": A page is one screenful of rows. Page size is how many rows are on each page. With 240 rows and a page size of 25, you get 10 pages (the last one holds only 15). "Pagination" is just the act of splitting the rows up this way and giving the user buttons to move between the chunks — the same idea as page numbers at the bottom of search results.
When should I use pagination?
Because virtualization already makes huge datasets fast, pagination is about how users prefer to navigate, not speed. Reach for it when:
- Users expect "page 3 of 10"-style navigation (reports, admin tables).
- You want a predictable, fixed grid height instead of a long scroll.
- Your data comes from a server one page at a time (see Chapter 14).
Leave it off (the default) when an endless, smooth scroll feels more natural — like a feed or a large lookup list.
Turning pagination on
const grid = createGrid<User>({
columns,
data,
pagination: {
enabled: true,
pageSize: 25,
},
})
Pagination options
These go in the pagination group:
| Property | Type | Default | Meaning |
|---|---|---|---|
enabled |
boolean |
false |
Turn pagination on |
pageSize |
number |
25 |
Rows per page |
pageSizeOptions |
number[] |
[10, 25, 50, 100] |
Choices in the size dropdown |
showPageSizeSelector |
boolean |
true |
Show the "rows per page" dropdown |
showRowCount |
boolean |
true |
Show "1–25 of 100" text |
showPageNumbers |
boolean |
true |
Show numbered page buttons |
pagination: {
enabled: true,
pageSize: 50,
pageSizeOptions: [50, 100, 200],
showPageSizeSelector: true,
showRowCount: true,
showPageNumbers: true,
}
Controlling pages from code (the API)
api.setPage(3) // jump to page 3 (1-based)
api.setPageSize(100) // change rows per page
api.nextPage()
api.previousPage()
api.firstPage()
api.lastPage()
// Read the full pagination state
const state = api.getPaginationState()
// {
// currentPage: 3,
// pageSize: 25,
// totalRows: 240,
// totalPages: 10,
// startRow: 51,
// endRow: 75,
// hasNextPage: true,
// hasPreviousPage: true,
// }
Reading the state object. Each field answers a question you'd otherwise have to compute yourself:
currentPage/totalPages— "page 3 of 10".pageSize— rows shown per page right now.totalRows— how many rows exist after filtering (not the raw data length).startRow/endRow— the 1-based range on screen ("showing 51–75").hasNextPage/hasPreviousPage— booleans handy for disabling your own Next/Prev buttons at the ends.Note pages are 1-based: the first page is
1, not0.
Reacting to page changes
onPageChange fires both when the page changes and when the page size changes:
events: {
onPageChange: (page, pageSize) => {
console.log(`Now on page ${page}, showing ${pageSize} per page`)
},
}
A minimal example
const grid = createGrid<User>({
columns: [
{ field: 'name', header: 'Name' },
{ field: 'email', header: 'Email' },
],
data: thousandsOfUsers,
pagination: {
enabled: true,
pageSize: 20,
pageSizeOptions: [20, 50, 100],
},
events: {
onPageChange: (page) => console.log('page', page),
},
})
Pagination and filtering together
When the user changes a filter, the current page can easily no longer make sense — page 5 might not exist anymore once the filter removes most of the rows. EliteGrid handles this for you: applying a filter automatically returns to page 1. onFilterChange fires first, and — only if the page actually changed (i.e. you weren't already on page 1) — onPageChange follows right after:
events: {
onFilterChange: () => console.log('1: filters changed'),
onPageChange: (page) => console.log('2: now on page', page), // only if page moved
}
Sorting does not do this. Changing the sort re-orders the current page in place and leaves you on the same page number — there's no reason to jump back to page 1 just because the rows on your current page are now in a different order.
If you're driving your own "page X of Y" UI outside the grid, read api.getPaginationState() inside onPageChange (or onFilterChange) rather than assuming the page number you last set is still current.
Building your own pager UI
The built-in pager covers most needs, but if you want fully custom controls (e.g. to match an existing design system), hide the default one and drive pagination entirely from the API:
pagination: {
enabled: true,
pageSize: 25,
showPageNumbers: false,
showRowCount: false,
showPageSizeSelector: false,
}
function CustomPager({ api }: { api: GridAPI<User> }) {
const [state, setState] = useState(api.getPaginationState())
useEffect(() => {
grid.updateEvents({
onPageChange: () => setState(api.getPaginationState()),
})
}, [])
return (
<div>
<button disabled={!state.hasPreviousPage} onClick={() => api.previousPage()}>
←
</button>
<span>
Page {state.currentPage} of {state.totalPages}
</span>
<button disabled={!state.hasNextPage} onClick={() => api.nextPage()}>
→
</button>
</div>
)
}
Live example
42 rows with a custom pageSize and pageSizeOptions — use the page-size dropdown and prev/next controls at the bottom of the grid.
import { createGrid, Grid } from '@elitegrid/react'
import '@elitegrid/react/styles.css'
interface Contact {
id: number
name: string
company: string
city: string
}
const COMPANIES = ['Acme Corp', 'Globex', 'Initech', 'Umbrella', 'Stark Industries', 'Wayne Enterprises']
const CITIES = ['Austin', 'Denver', 'Seattle', 'Chicago', 'Miami', 'Boston']
const NAMES = ['Ada', 'Alan', 'Grace', 'Linus', 'Margaret', 'Dennis', 'Barbara', 'Ken']
const contacts: Contact[] = Array.from({ length: 42 }, (_, i) => ({
id: i + 1,
name: `${NAMES[i % NAMES.length]} ${String.fromCharCode(65 + (i % 26))}.`,
company: COMPANIES[i % COMPANIES.length],
city: CITIES[i % CITIES.length],
}))
const grid = createGrid<Contact>({
columns: [
{ field: 'name', header: 'Name', size: { flex: 1.5 } },
{ field: 'company', header: 'Company', size: { flex: 1.5 } },
{ field: 'city', header: 'City' },
],
data: contacts,
pagination: { enabled: true, pageSize: 10, pageSizeOptions: [5, 10, 20] },
})
export default function App() {
return (
<div style={{ height: 440 }}>
<Grid grid={grid} />
</div>
)
}
Common pagination mistakes
| Symptom | Cause | Fix |
|---|---|---|
| "Page 3 of 3" but the page looks empty | You jumped to a page with api.setPage() before data/filters settled |
Read totalPages from api.getPaginationState() after the change, or clamp your target page to it |
| Custom pager shows a stale page number | Reading getPaginationState() once instead of on every onPageChange |
Re-read the state inside the event handler, as in the example above |
| Page resets unexpectedly while typing in a filter | Expected — see "Pagination and filtering/sorting together" above | Nothing to fix; this is intentional so users don't land on a now-invalid page |
pageSize change doesn't seem to do anything |
pagination.enabled is false |
Pagination options only take effect once enabled: true |
Next: 06 · Row Selection