What is your developer-dependent CMS actually costing you? Calculate your costs and get a full report

Agility CMS

How to Handle Pagination in React (Client-Side Only)

Tips, codes, and best practices

Agility CMS

Agility CMS

Canada's Original Headless CMS

Developer ToolsDeveloper ToolsHow To Developer
Man working with React on a laptop

When you fetch a large dataset in a React app, rendering all items at once not only hurts performance but also overwhelms users. Client-side pagination breaks your data into manageable chunks, providing a snappy, scroll-free browsing experience without additional server requests.

In this tutorial, we’ll build:

  • A reusable <Pagination> component.

  • A custom usePagination hook.

  • An example list that paginates 100 items.

Let’s dive in.


1. Why Client-Side Pagination?

  • Performance: Only a subset of data renders at once, reducing DOM nodes and improving frame rates.

  • User Experience: Clear “Next” / “Previous” controls and page numbers let users jump easily.

  • No Extra Requests: Once data is loaded, page transitions are instant.

Note: For server-side pagination (large or infinite data), see our Next.js guide:


2. Fetching & Storing Data

Assume you’ve already fetched an array of items—say, posts—from an API and stored it in state:

jsx

CopyEdit

import { useEffect, useState } from 'react';

function App() {
  const [items, setItems] = useState([]);

  useEffect(() => {
    async function fetchItems() {
      const res = await fetch('/api/posts');
      const data = await res.json();
      setItems(data);
    }
    fetchItems();
  }, []);

  // Example: render a simple list of items
  return (
    
      {items.map((item, index) => (
        {item.title}
      ))}
    
  );
}

With items loaded, we can plug into our pagination logic.


3. Creating the usePagination Hook

Encapsulate pagination state and calculations:

jsx

CopyEdit

import { useMemo, useState } from 'react';

export function usePagination({ items, itemsPerPage = 10 }) {
  const [currentPage, setCurrentPage] = useState(1);

  const maxPage = useMemo(
    () => Math.ceil(items.length / itemsPerPage),
    [items.length, itemsPerPage]
  );

  const currentItems = useMemo(() => {
    const start = (currentPage - 1) * itemsPerPage;
    return items.slice(start, start + itemsPerPage);
  }, [items, currentPage, itemsPerPage]);

  function goToPage(page) {
    const valid = Math.min(Math.max(page, 1), maxPage);
    setCurrentPage(valid);
  }

  return { currentItems, currentPage, maxPage, goToPage };
}

How it works:

  • maxPage computes total pages.

  • currentItems slices the full array for the active page.

  • goToPage ensures the page number stays within bounds.


4. Building a <Pagination> Component

Render controls for previous/next and page numbers:

jsx

CopyEdit

import React from 'react';

export default function Pagination({ currentPage, maxPage, goToPage }) {
  const pages = Array.from({ length: maxPage }, (_, i) => i + 1);

  return (
    
       goToPage(currentPage - 1)} disabled={currentPage === 1}>
        Previous
      

      {pages.map(page => (
         goToPage(page)}
          aria-current={page === currentPage ? 'page' : undefined}
        >
          {page}
        
      ))}

       goToPage(currentPage + 1)} disabled={currentPage === maxPage}>
        Next
      
    
  );
}

Feel free to style buttons or replace them with links if you integrate React Router.


5. Putting It All Together

Use both the hook and component in your main list:

jsx

CopyEdit

import React from 'react';
import { usePagination } from './usePagination';
import Pagination from './Pagination';

export default function PaginatedList({ items }) {
  const { currentItems, currentPage, maxPage, goToPage } = usePagination({
    items,
    itemsPerPage: 10,
  });

  return (
    
      
        {currentItems.map(item => (
          {item.title}
        ))}
      

      
    
  );
}

In your top-level component (e.g. App), pass the fetched items into <PaginatedList items={items} />.


6. Enhancements & Best Practices

  • Page Jump Input: Let users type a page number to jump directly.

  • Accessibility: Use ARIA attributes (aria-current, aria-label) for screen readers.

  • URL Sync: Reflect currentPage in the URL query string for shareable links and back/forward navigation—see routing in React.

  • Custom Hooks: Extend usePagination to support dynamic itemsPerPage or server data loading fallback.


Conclusion

Client-side pagination in React is straightforward once you encapsulate logic in a custom hook and render a simple navigation component.

You get better performance, a cleaner UI, and no extra network overhead. For more advanced layouts, like infinite scroll or hybrid paging, consider integrating libraries such as React Query or TanStack Table’s pagination features.

Happy coding!