Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

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:

src/api/magentoApi.js
// 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,
  };
}
src/api/magentoApi.js
// 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.