Pagination in React: Page Navigation for Lists
Pagination in React
~12 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
Our fetchProducts() so far always loads just the first 20 products. For a shop with hundreds of products that's not enough – we need "next"/"previous" navigation through the results, i.e. pagination.
Extending magentoApi.js: accepting a page number
Extend fetchProducts() with a parameter for the current page. Both options from chapter 22 support pagination, just with different parameter names:
// Option A (Magento):
export async function fetchProducts(page = 1, pageSize = 6) {
const response = await fetch(
`${BASE_URL}/products?searchCriteria[pageSize]=${pageSize}&searchCriteria[currentPage]=${page}`
);
if (!response.ok) {
throw new Error(`Magento API error: ${response.status}`);
}
const data = await response.json();
return {
items: data.items.map(mapMagentoProduct),
totalCount: data.total_count,
};
}// Option B (dummyjson.com):
export async function fetchProducts(page = 1, pageSize = 6) {
const skip = (page - 1) * pageSize;
const response = await fetch(`${BASE_URL}/products?limit=${pageSize}&skip=${skip}`);
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
const data = await response.json();
return {
items: data.products.map(mapMagentoProduct),
totalCount: data.total,
};
}Two different API philosophies become visible here: Magento works with "page number + page size" (currentPage/pageSize), dummyjson.com with "offset + count" (skip/limit – "skip the first N entries"). skip = (page - 1) * pageSize converts between the two concepts. IMPORTANT: fetchProducts() now returns an OBJECT with items AND totalCount, instead of just an array – the calling page needs to account for that.
ProductListPage.jsx: adding page navigation
const PAGE_SIZE = 6;
// ... inside the component, add state:
const [page, setPage] = useState(1);
const [totalCount, setTotalCount] = useState(0);
// ... in the useEffect, depending on page:
useEffect(() => {
async function load() {
setLoading(true);
try {
const { items, totalCount } = await fetchProducts(page, PAGE_SIZE);
setProducts(items);
setTotalCount(totalCount);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
load();
}, [page]);
// ... computed total pages:
const totalPages = Math.ceil(totalCount / PAGE_SIZE);
// ... in the JSX, after the product grid:
<div className="pagination">
<button disabled={page === 1} onClick={() => setPage(page - 1)}>
Previous
</button>
<span>Page {page} of {totalPages}</span>
<button disabled={page >= totalPages} onClick={() => setPage(page + 1)}>
Next
</button>
</div>[page] as a dependency in the useEffect is the key: every time page changes from a click on "next"/"previous", the effect automatically reloads the matching products. disabled={{page === 1}} and disabled={{page >= totalPages}} disable the buttons at the edges, instead of requesting a page that doesn't exist.
Achtung: An important misconception beginners often have: pagination does NOT filter the already-loaded products array client-side, it requests fresh data from the server on EVERY page change. For small amounts of data, "load everything, then split client-side" would technically also work, but doesn't scale to thousands of products – that's why the API itself handles the splitting.
Tipp: Our search (query) still only filters the currently loaded page, not the entire product catalog – a "real", server-side search across all pages would need its own API parameter (with Magento, e.g. searchCriteria[filterGroups]) and is beyond the scope of this beginner tutorial.