react ui components
render-propsuseMemocomponent-api
Pagination
Build a reusable pagination component for a large list.
What it tests
Component API design. Slicing an array by page is trivial; the actual question is how the component stays reusable when the next screen needs different markup for both the rows and the controls.
The approach
Headless, via render props. Pagination owns the page state and the maths and
renders nothing opinionated — the caller supplies renderItem and
renderPagination, and receives the state and movers as arguments:
renderPagination({
page,
totalPages,
goTo,
next: () => goTo(page + 1),
prev: () => goTo(page - 1),
});goTo clamps to the valid range in one place, so next, prev and a direct
page click can't each invent their own bounds check. The slice is memoised on
[data, page, pageSize].
Where it usually goes wrong
- Baking in the button markup. Works once, gets copy-pasted the second time.
- Bounds checks at every call site. Guard inside
goToinstead. - Not resetting on data change. If
datashrinks while you're on page 20,pageis now out of range and the list renders empty. - Rendering every page number. 2,000 items at 10 a page is 200 buttons; real pagination windows them around the current page.
Full solution
2 files from src/folders/Pagination.
index.jsx
import { Pagination } from "./pagination";
const Data = [...new Array(2000)].map((_, i) => i + 1);
export default function App() {
return (
<Pagination
data={Data}
pageSize={10}
renderItem={(item) => (
<li key={item} className="list-none">
{item}
</li>
)}
renderPagination={({ page, totalPages, goTo, next, prev }) => (
<div className="mt-4 flex gap-2">
<button onClick={prev} disabled={page === 1}>
Prev
</button>
{[...new Array(Math.min(totalPages, 10))].map((_, idx) => {
const p = idx + 1;
return (
<button
key={p}
onClick={() => goTo(p)}
style={{
background: page === p ? "red" : "white",
border: "1px solid black",
padding: "4px 8px",
}}
>
{p}
</button>
);
})}
<button onClick={next} disabled={page === totalPages}>
Next
</button>
</div>
)}
/>
);
}pagination.jsx
import { useMemo, useState } from "react";
export function Pagination({
data = [],
pageSize = 10,
initialPage = 1,
renderItem,
renderPagination,
}) {
const [page, setPage] = useState(initialPage);
const totalPages = Math.ceil(data.length / pageSize);
const pageData = useMemo(() => {
const start = (page - 1) * pageSize;
return data.slice(start, start + pageSize);
}, [data, page, pageSize]);
function goTo(p) {
if (p < 1 || p > totalPages) return;
setPage(p);
}
return (
<div>
<div>{pageData.map((item, idx) => renderItem(item, idx))}</div>
{renderPagination({
page,
totalPages,
goTo,
next: () => goTo(page + 1),
prev: () => goTo(page - 1),
})}
</div>
);
}share this post