Part V β The Craft Β· Lesson 10.12
Pagination Part-2
In one lineBuild it: URL-driven state, an ellipsis page range, and a keyboard-accessible nav element.
Where you've seen itAny search results footer: β 1 β¦ 4 [5] 6 β¦ 92 β.
Diagram
COMPONENT STRUCTURE β page state lives in the URL
URL ?page=5&sort=price&q=shoes
β useSearchParams()
βΌ
ββββββββββββββββββββββββββββββββββββββ
β usePagination(total, page, size) β pure: returns the range
ββββββββββββββββ¬ββββββββββββββββββββββ
βΌ
ββββββββββββββββββββββββββββββββββββββ
β <Pagination> β
β β Prev 1 β¦ 4 [5] 6 β¦ 92 Next β β
ββββββββββββββββ¬ββββββββββββββββββββββ
βΌ setSearchParams({page: 6})
URL changes βββΊ query refetches (6.9) βββΊ table re-renders
Never store the page in component state β the URL is the state.THE ELLIPSIS WINDOW β always show the same number of slots
total 92, sibling count 1, boundaries 1
page 1 [1] 2 3 β¦ 92
page 5 1 β¦ 4 [5] 6 β¦ 92
page 50 1 β¦ 49 [50] 51 β¦ 92
page 92 1 β¦ 90 91 [92]
Rule: first + last always visible Β· window of (siblings) around
current Β· ellipsis fills the gaps Β· fixed width so the
control never jumps as pages change (CLS β 5.1)STATES TO HANDLE
loading keep the OLD rows visible + dim them
(blanking the table causes a jarring layout jump)
empty "No results for X" + a way to clear filters
error keep the pager usable, offer retry
single page hide the control entirely
invalid ?page=999 β clamp to last page, don't crashHow it works
- Read page and size from the URL, so refresh, back, and shared links all reproduce the exact view.
- Compute the range in a pure hook β easy to unit test with no rendering (4.2).
- Render a
<nav aria-label="Pagination">containing real<a>or<button>elements; mark the current one witharia-current="page". - Keep placeholders stable so the control doesn't resize between pages.
- Prefetch the next page on hover and preserve scroll position when navigating.
function usePagination({ total, page, siblings = 1, boundaries = 1 }) {
const pages = Math.ceil(total / size);
if (pages <= 5 + siblings * 2) return range(1, pages); // no ellipsis
const left = Math.max(page - siblings, boundaries + 2);
const right = Math.min(page + siblings, pages - boundaries - 1);
return [
...range(1, boundaries),
...(left > boundaries + 1 ? ['β¦'] : []),
...range(left, right),
...(right < pages - boundaries ? ['β¦'] : []),
...range(pages - boundaries + 1, pages),
];
}Trade-offs
| Decision | Consequence |
|---|---|
| Page in URL | Shareable, back-button correct |
| Page in component state | Breaks refresh and sharing |
<a href> links | Crawlable, middle-clickable |
<button> only | SPA-only; loses SEO and open-in-new-tab |
| Keep old rows while loading | No layout jump, feels faster |
Interview angle
"Build a pagination component."
- Start with the API:
total,page,pageSize,onChangeβ and state that page lives in the URL, not in the component. - Extract the ellipsis range into a pure function so it's unit-testable, and keep the slot count fixed to avoid layout shift.
- Use
<nav>witharia-current="page"and real links, so keyboard, screen reader, and middle-click all behave correctly.
Recap
- URL is the source of truth for page, size, sort, and filters.
- A pure range function keeps the ellipsis logic testable and the control stable.
- Dim old rows instead of blanking the table while the next page loads.