# React Pagination Example: Best Practices for State Management and Component Structure

> Learn to build a React pagination component with best practices. Master state management using useState, useMemo, and useCallback for efficient data slicing and navigation.

- Repository: [Meta/react](https://github.com/facebook/react)
- Tags: best-practices
- Published: 2026-02-16

---

**A production-ready React pagination component leverages `useState` for page tracking, `useMemo` for data slicing, and `useCallback` for stable navigation handlers, while isolating presentation logic in pure child components.**

Implementing pagination in React requires careful orchestration of state to prevent performance bottlenecks and ensure predictable behavior. This react pagination example demonstrates patterns derived directly from the facebook/react source code, showing how core hooks work together to create scalable, testable components that separate container logic from presentational concerns.

## Core Hooks for Pagination State

The React source code in [`packages/react/src/ReactHooks.js`](https://github.com/facebook/react/blob/main/packages/react/src/ReactHooks.js) provides the foundation for all state management patterns in this example. Understanding these hook implementations explains why specific patterns yield better performance for pagination UIs.

### Tracking Current Page with useState

The current page number represents simple scalar state that updates directly in response to user interaction. According to the React source, `useState` resolves through the dispatcher at lines 66-71 in [`packages/react/src/ReactHooks.js`](https://github.com/facebook/react/blob/main/packages/react/src/ReactHooks.js):

```javascript
// From packages/react/src/ReactHooks.js#L66-L71
export function useState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

```

For pagination, initialize `useState` with `1` and update it via navigation handlers to ensure predictable state transitions.

### Memoizing Data Slices with useMemo

Slicing large arrays on every render creates O(n) performance bottlenecks. The `useMemo` hook, implemented at lines 143-148 in [`packages/react/src/ReactHooks.js`](https://github.com/facebook/react/blob/main/packages/react/src/ReactHooks.js), caches expensive computations:

```javascript
// From packages/react/src/ReactHooks.js#L143-L148
export function useMemo<T>(
  create: () => T,
  deps: Array<mixed> | void | null,
): T {
  const dispatcher = resolveDispatcher();
  return dispatcher.useMemo(create, deps);
}

```

Apply `useMemo` to calculate the current page's data slice, depending on the source array, current page, and page size to prevent unnecessary array operations.

### Stabilizing Callbacks with useCallback

Navigation handlers passed to child components must maintain referential equality to prevent unnecessary re-renders. The `useCallback` implementation at lines 135-140 in [`packages/react/src/ReactHooks.js`](https://github.com/facebook/react/blob/main/packages/react/src/ReactHooks.js) serves this purpose:

```javascript
// From packages/react/src/ReactHooks.js#L135-L140
export function useCallback<T>(
  callback: T,
  deps: Array<mixed> | void | null,
): T {
  const dispatcher = resolveDispatcher();
  return dispatcher.useCallback(callback, deps);
}

```

Wrap `goToPage`, `nextPage`, and `previousPage` functions with `useCallback`, including `totalPages` in the dependency array to ensure stable references for memoized child components.

### Handling Side Effects with useEffect

Synchronizing pagination state with external systems—such as browser URLs or server requests—requires `useEffect`. The implementation at lines 87-101 in [`packages/react/src/ReactHooks.js`](https://github.com/facebook/react/blob/main/packages/react/src/ReactHooks.js) handles these imperative operations:

```javascript
// From packages/react/src/ReactHooks.js#L87-L101
export function useEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  const dispatcher = resolveDispatcher();
  return dispatcher.useEffect(create, deps);
}

```

Use this hook to update `window.history` when the page changes or to trigger data fetches in server-side pagination scenarios, always providing cleanup functions to prevent memory leaks.

## Client-Side Pagination Implementation

For datasets that fit in memory, client-side pagination provides instant navigation without network latency. This implementation demonstrates the container-presentational pattern with strict separation of concerns.

```typescript
import React, {useState, useMemo, useCallback} from 'react';

// ---- Pagination UI (pure) -------------------------------------------------
type PagerProps = {
  current: number;
  totalPages: number;
  onPageChange: (page: number) => void;
};

function Pager({current, totalPages, onPageChange}: PagerProps) {
  const pages = [...Array(totalPages)].map((_, i) => i + 1);

  return (
    <nav aria-label="Pagination">
      <button
        onClick={() => onPageChange(current - 1)}
        disabled={current === 1}
      >
        Prev
      </button>

      {pages.map(p => (
        <button
          key={p}
          onClick={() => onPageChange(p)}
          aria-current={p === current ? 'page' : undefined}
          style={{fontWeight: p === current ? 'bold' : 'normal'}}
        >
          {p}
        </button>
      ))}

      <button
        onClick={() => onPageChange(current + 1)}
        disabled={current === totalPages}
      >
        Next
      </button>
    </nav>
  );
}

// ---- Pagination container -------------------------------------------------
type Props<T> = {
  items: T[];
  pageSize?: number;
  renderItem: (item: T) => React.ReactNode;
};

export function PaginatedList<T>({
  items,
  pageSize = 10,
  renderItem,
}: Props<T>) {
  // 1️⃣ current page state (useState) — see ReactHooks.js source
  const [currentPage, setCurrentPage] = useState(1);

  // 2️⃣ total page count (derived)
  const totalPages = Math.ceil(items.length / pageSize);

  // 3️⃣ memoised slice for the current page (useMemo)
  const currentItems = useMemo(() => {
    const start = (currentPage - 1) * pageSize;
    return items.slice(start, start + pageSize);
  }, [items, currentPage, pageSize]);

  // 4️⃣ stable navigation handler (useCallback)
  const goToPage = useCallback(
    (page: number) => {
      const safePage = Math.max(1, Math.min(page, totalPages));
      setCurrentPage(safePage);
    },
    [totalPages],
  );

  return (
    <div>
      <ul>
        {currentItems.map((item, idx) => (
          <li key={idx}>{renderItem(item)}</li>
        ))}
      </ul>

      <Pager
        current={currentPage}
        totalPages={totalPages}
        onPageChange={goToPage}
      />
    </div>
  );
}

```

This example demonstrates how `useState`, `useMemo`, and `useCallback` work together to create a performant pagination component. The `Pager` UI remains pure and stateless, receiving all data and callbacks through props, while `PaginatedList` encapsulates all state management logic.

## Server-Side Pagination with URL Synchronization

For large datasets, server-side pagination reduces initial load time and memory pressure. This implementation adds URL synchronization using `useSearchParams` from React Router, allowing users to bookmark and share specific pages.

```typescript
import React, {useState, useEffect, useMemo, useCallback} from 'react';
import {useSearchParams} from 'react-router-dom';

// Mock API – in a real app this would hit a back‑end
async function fetchPage(page: number, size: number) {
  const response = await fetch(
    `/api/items?page=${page}&size=${size}`,
  );
  return response.json(); // {items: [], totalCount: number}
}

export function ServerPaginatedList({pageSize = 20, renderItem}) {
  const [searchParams, setSearchParams] = useSearchParams();
  const initialPage = Number(searchParams.get('page')) || 1;

  const [currentPage, setCurrentPage] = useState(initialPage);
  const [data, setData] = useState({items: [], totalCount: 0});
  const totalPages = Math.ceil(data.totalCount / pageSize);

  // Side‑effect: load data when page changes (useEffect)
  useEffect(() => {
    let cancelled = false;
    fetchPage(currentPage, pageSize).then(res => {
      if (!cancelled) setData(res);
    });
    return () => {
      cancelled = true;
    };
  }, [currentPage, pageSize]);

  // Keep URL in sync
  useEffect(() => {
    setSearchParams({page: String(currentPage)});
  }, [currentPage, setSearchParams]);

  const goToPage = useCallback(
    (page: number) => setCurrentPage(Math.max(1, Math.min(page, totalPages))),
    [totalPages],
  );

  return (
    <>
      <ul>
        {data.items.map(item => (
          <li key={item.id}>{renderItem(item)}</li>
        ))}
      </ul>

      <Pager
        current={currentPage}
        totalPages={totalPages}
        onPageChange={goToPage}
      />
    </>
  );
}

```

This server-side implementation leverages `useEffect` for data fetching, as implemented in [`packages/react/src/ReactHooks.js`](https://github.com/facebook/react/blob/main/packages/react/src/ReactHooks.js) lines 87-101. The cleanup function prevents memory leaks by cancelling stale requests when the component unmounts or the page changes rapidly.

## Component Architecture Best Practices

Separating stateful logic from presentational components creates a maintainable pagination system. The container-presentational pattern, combined with React's memoization APIs, ensures optimal performance even with large datasets.

### State Management Strategy

- **Current page number**: Manage with `useState` in the container component, initialized from URL parameters if server-side.
- **Page size**: Store as a constant or prop, rarely changing after mount.
- **Derived data slice**: Compute with `useMemo` to avoid re-slicing arrays on every render.
- **Navigation callbacks**: Wrap with `useCallback` to maintain stable references for child components.
- **Side effects**: Handle data fetching and URL updates in `useEffect` with proper cleanup functions.

### Performance Optimizations

1. **Memoize expensive calculations**: The `useMemo` implementation in [`packages/react/src/ReactHooks.js`](https://github.com/facebook/react/blob/main/packages/react/src/ReactHooks.js) (lines 143-148) caches the array slice operation, preventing O(n) work on every render.

2. **Stabilize callbacks**: `useCallback` (lines 135-140 in [`packages/react/src/ReactHooks.js`](https://github.com/facebook/react/blob/main/packages/react/src/ReactHooks.js)) ensures that navigation handlers maintain referential equality, allowing `React.memo` to effectively prevent re-renders of pure UI components.

3. **Abortable requests**: When using `useEffect` for server-side pagination, always implement a cancellation mechanism using the cleanup function to prevent race conditions and memory leaks.

## Summary

- **Use `useState`** in [`packages/react/src/ReactHooks.js`](https://github.com/facebook/react/blob/main/packages/react/src/ReactHooks.js) (lines 66-71) to track the current page as simple scalar state.
- **Apply `useMemo`** (lines 143-148) to derive the current page's data slice, preventing expensive array operations on every render.
- **Wrap handlers with `useCallback`** (lines 135-140) to maintain stable references for memoized child components.
- **Leverage `useEffect`** (lines 87-101) for side effects like data fetching and URL synchronization, always providing cleanup functions.
- **Separate concerns** by isolating state logic in container components and rendering logic in pure presentational components.

## Frequently Asked Questions

### How do I prevent unnecessary re-renders in a React pagination component?

Use `useCallback` to memoize navigation functions and `useMemo` for derived data calculations. According to the React source in [`packages/react/src/ReactHooks.js`](https://github.com/facebook/react/blob/main/packages/react/src/ReactHooks.js) (lines 135-140), `useCallback` ensures that child components receiving these functions as props won't re-render unless the dependencies actually change. Additionally, split your component into a stateful container and a pure presentational component that can be wrapped in `React.memo`.

### Should I use useState or useReducer for pagination state?

For standard pagination with next/previous buttons and direct page selection, `useState` is sufficient and simpler. The implementation in [`packages/react/src/ReactHooks.js`](https://github.com/facebook/react/blob/main/packages/react/src/ReactHooks.js) (lines 66-71) shows that `useState` is optimized for scalar values that update directly. However, if your pagination includes complex state machines with loading states, error handling, and jump-to-first/last logic, `useReducer` (lines 73-80) provides better structure for related state transitions.

### How do I synchronize pagination state with the URL?

Use `useSearchParams` from React Router to read the initial page from the URL, and `useEffect` to update the URL when the page changes. Initialize your `useState` with the value from search parameters, then use an effect that calls `setSearchParams` whenever `currentPage` updates. This approach, leveraging the `useEffect` implementation in [`packages/react/src/ReactHooks.js`](https://github.com/facebook/react/blob/main/packages/react/src/ReactHooks.js) (lines 87-101), ensures the browser's back button works correctly and allows users to bookmark specific pages.

### What is the best way to handle server-side pagination in React?

Implement an abortable fetch pattern using `useEffect` with a cleanup function. Store the current page in `useState`, then use `useEffect` to fetch data whenever the page or page size changes. Return a cleanup function that sets a cancellation flag to prevent state updates on unmounted components or stale requests. Combine this with `useMemo` to calculate `totalPages` from the API response, and `useCallback` for the `goToPage` handler to maintain stable references for child components.