← Back to React Course | Chapter 10: Data Fetching & Async | Lesson 9 of 10

Pagination and Infinite Scroll

Pagination is like reading a book one page at a time instead of trying to unfold the entire story onto one giant poster.

Tracking the Current Page

A simple page state variable (starting at 1) tracks which page of data is currently displayed. Changing this value, usually via Next/Previous buttons, is what triggers showing (or fetching) a different slice of the full dataset.

Note: Keep page state as a plain number — it's easy to reason about and simple to reset.

Warning: Forgetting to reset page back to 1 when a search or filter changes can leave the user looking at 'page 5' of a filtered result that only has 2 pages.

Example: Tracking the Current Page

markup
<!DOCTYPE html>
<html>
<head>
  <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
  <div id="root"></div>
  <script type="text/babel">
function App() {
  const [page, setPage] = React.useState(1);
  return (
    <div>
      <p>Current page: {page}</p>
      <button onClick={() => setPage(p => p + 1)}>Next</button>
    </div>
  );
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  </script>
</body>
</html>

Slicing Client-Side Data by Page

When all the data is already loaded on the client, pagination is just a matter of slicing the array based on the current page and a fixed page size, using Array.slice() to grab just the relevant chunk to display.

Note: Compute the slice indices as (page - 1) * pageSize and page * pageSize.

Warning: This client-side slicing approach only works when the full dataset is already loaded — for large datasets, server-side pagination (fetching just one page at a time) is usually better.

Example: Slicing Client-Side Data by Page

markup
<!DOCTYPE html>
<html>
<head>
  <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
  <div id="root"></div>
  <script type="text/babel">
function App() {
  const items = Array.from({ length: 23 }, (_, i) => "Item " + (i + 1));
  const [page, setPage] = React.useState(1);
  const pageSize = 5;
  const pageItems = items.slice((page - 1) * pageSize, page * pageSize);
  return (
    <div>
      <ul>{pageItems.map(i => <li key={i}>{i}</li>)}</ul>
      <button onClick={() => setPage(p => p + 1)}>Next</button>
    </div>
  );
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  </script>
</body>
</html>

Disabling Buttons at the Boundaries

Computing the total number of pages (from the total item count and page size) lets you disable the Previous button on page 1 and the Next button on the last page, preventing users from navigating to pages that don't exist.

Note: Math.ceil(totalItems / pageSize) gives the correct total page count, rounding up for a partial final page.

Warning: Without boundary checks, clicking Next repeatedly past the last page shows an empty page with no items and no clear explanation why.

Example: Disabling Buttons at the Boundaries

markup
<!DOCTYPE html>
<html>
<head>
  <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
  <div id="root"></div>
  <script type="text/babel">
function App() {
  const items = Array.from({ length: 12 }, (_, i) => "Item " + (i + 1));
  const [page, setPage] = React.useState(1);
  const pageSize = 5;
  const totalPages = Math.ceil(items.length / pageSize);
  return (
    <div>
      <p>Page {page} of {totalPages}</p>
      <button onClick={() => setPage(p => p - 1)} disabled={page === 1}>Prev</button>
      <button onClick={() => setPage(p => p + 1)} disabled={page === totalPages}>Next</button>
    </div>
  );
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  </script>
</body>
</html>
Common Mistakes
  1. Fetching all pages upfront instead of only the current one, defeating the purpose of pagination.
  2. Forgetting to reset the current page back to 1 when a filter or search term changes.
  3. Not disabling the Next button on the last page, letting users request pages that don't exist.
Chapter Summary
  • Pagination splits a large dataset into pages, fetching or showing only one page's worth at a time.
  • A page state variable tracks which page is currently active.
  • Next/Previous buttons increment or decrement the page, usually re-triggering a fetch for that page's data.
  • Disabling navigation buttons at the first/last page prevents requesting out-of-range pages.
Browser Support

No React-version restriction — built from useState and (optionally) a data-fetching hook.

Login to run this code

C/C++/Java/PHP execution requires a free account. Your code is saved — you'll land right back in the editor after logging in.