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

Tips, codes, and best practices

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

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!

Agility CMS
About the Author
Agility CMS

Agility CMS is Canada's original headless CMS platform. Since 2002, Agility has helped companies across Canada and around the world better manage their content. Marketers are free to create the content they want, when they want it. Developers are empowered to build what they want, how they want.

Take the next steps

We're ready when you are. Get started today, and choose the best learning path for you with Agility CMS.