Skip to main content

Comprehensive Pagination Component Design and Implementation

Detailed low-level design for a scalable, accessible pagination component with React and TypeScript support.

This document presents a comprehensive low-level design for a flexible pagination component that handles large datasets efficiently while maintaining accessibility standards and responsive behavior.

1. Problem Statement & Requirements

Clarifying questions

  1. What's the target browser support matrix? (e.g., IE11, modern browsers only?)
  2. Do we need to support server-side rendering (SSR) or is this client-only?
  3. Should pagination work with real-time data updates or static datasets?
  4. Is offline support required? How should the component behave when network is unavailable?
  5. What's the expected accessibility level? Should it be fully keyboard navigable and screen reader compatible?
  6. Do we need i18n support for pagination labels (e.g., "Next", "Previous", page numbers)?
  7. Are there existing design system components we should integrate with?

Functional requirements

  • Display a set of pages with navigation controls (Previous, Next, First, Last)
  • Show current page indicator and total page count
  • Allow direct page number input or selection from a dropdown
  • Support keyboard navigation (arrow keys, Enter, Space)
  • Handle large datasets efficiently with virtualized lists when appropriate
  • Support responsive design that adapts to different screen sizes
  • Provide visual feedback for loading states and error conditions

Non-functional requirements

  • Performance: Should handle 10k+ pages efficiently with minimal DOM elements
  • Accessibility: Must meet WCAG 2.1 AA standards for keyboard navigation and screen readers
  • Scalability: Should work with both small (5 pages) and large (1000+ pages) datasets
  • Maintainability: Clean, composable component architecture with clear separation of concerns
  • Resilience: Graceful degradation when network issues occur or data is unavailable

Constraints and assumptions

  • Pagination will be used in conjunction with a data fetching system (e.g., React Query or custom API client)
  • Component assumes data is paginated on the backend (no client-side pagination)
  • Assumes consistent page size across all pages
  • Component will be used in a React 18+ environment with hooks support
  • No third-party UI component libraries are available for pagination controls

Out of scope

  • Full data fetching logic (this is just the UI component)
  • Server-side rendering implementation details
  • Complex filtering or sorting capabilities within pagination
  • Custom CSS framework integration (design system)
  • Advanced analytics tracking for page navigation events
  • Multi-page selection or batch operations within the pagination component itself

2. Component Architecture

graph TD
    A[Pagination] --> B[PageButton]
    A --> C[Ellipsis]
    A --> D[PageSizeSelector]
    A --> E[PageInfo]
    B --> F[PageButtonGroup]
    C --> F
    D --> F
    E --> F

Component Tree Structure

The Pagination component is a composite UI element that orchestrates several sub-components:

  1. Pagination (Container Component)
  2. PageButtonGroup (Smart Container)
  3. PageButton (Dumb Presentational)
  4. Ellipsis (Dumb Presentational)
  5. PageSizeSelector (Smart Container)
  6. PageInfo (Dumb Presentational)

Component Breakdown

Smart Components:

  • Pagination - orchestrates state and handles user interactions
  • PageButtonGroup - manages page button rendering logic
  • PageSizeSelector - handles page size changes

Dumb Components:

  • PageButton - renders individual page buttons with appropriate styling
  • Ellipsis - renders ellipsis indicators for skipped pages
  • PageInfo - displays current page and total items information

TypeScript Props Contracts

// Pagination Component Props
interface PaginationProps {
  currentPage: number;
  totalPages: number;
  onPageChange: (page: number) => void;
  pageSize?: number;
  onPageSizeChange?: (size: number) => void;
  showPageSizeSelector?: boolean;
  maxVisiblePages?: number;
  className?: string;
}

// PageButton Component Props
interface PageButtonProps {
  page: number;
  isActive: boolean;
  onClick: () => void;
  disabled?: boolean;
  className?: string;
}

// PageSizeSelector Component Props
interface PageSizeSelectorProps {
  pageSize: number;
  pageSizes: number[];
  onChange: (size: number) => void;
  className?: string;
}

UI State Machine

stateDiagram-v2
    [*] --> Idle
    Idle --> ActivePage : Click page button
    ActivePage --> Idle : Click page button
    Idle --> Loading : Fetching data
    Loading --> Idle : Data loaded
    Idle --> Error : API error
    Error --> Idle : Retry successful

Component Responsibilities

Pagination:

  • Manages overall pagination state (current page, total pages)
  • Handles user interactions for navigation
  • Coordinates between page buttons and size selector
  • Implements boundary logic for visible page range

PageButtonGroup:

  • Determines which pages to display based on current page and max visible
  • Handles ellipsis rendering logic
  • Manages page button generation within bounds

PageButton:

  • Renders individual page navigation buttons
  • Applies active state styling
  • Handles click events

Ellipsis:

  • Renders visual ellipsis indicators when pages are skipped
  • Maintains consistent styling with surrounding elements

PageSizeSelector:

  • Renders dropdown for changing page size
  • Handles selection changes
  • Updates parent state when changed

Reusability Decisions

The Pagination component is designed to be highly reusable:

  • Accepts all configuration via props
  • Supports custom styling through className
  • Works with any data source that provides page count and size
  • Can be used in various contexts (tables, lists, search results)
  • Implements proper accessibility attributes

Component Lifecycle Considerations

The Pagination component lifecycle includes:

  1. Mount: Initialize with provided page count and size
  2. Update: Handle changes to currentPage, totalPages, or pageSize
  3. Unmount: Cleanup any event listeners or subscriptions
  4. Re-render: Efficiently update only visible pages when state changes

The component uses React's memoization patterns to prevent unnecessary re-renders of individual page buttons when only the active page changes. The PageButtonGroup component implements useMemo to cache page button generation logic based on current state.

3. State Management Design

Local state vs. global state decisions

For a Pagination component, the core pagination state (current page, total pages, items per page) should be managed locally within the component to avoid unnecessary global state pollution. However, when pagination is part of a larger data-driven feature (like a table or list view), the page state should be lifted to the parent container to coordinate with other data operations.

State management approach

For standalone pagination, we'll use React's built-in useState and useEffect hooks with a custom hook pattern. For pagination within larger data views, we'll integrate with React Query or Redux Toolkit to maintain consistent state across the application.

The custom hook approach provides:

  • Clear separation of concerns
  • Reusable pagination logic across components
  • Easy testing and mocking
  • No additional dependencies for simple use cases

State shape and normalization strategy

interface PaginationState {
  currentPage: number;
  totalPages: number;
  itemsPerPage: number;
  totalItems: number;
  hasNextPage: boolean;
  hasPrevPage: boolean;
}

interface PaginationConfig {
  initialPage?: number;
  itemsPerPage?: number;
  totalItems?: number;
}

interface PaginationResult {
  currentPage: number;
  totalPages: number;
  hasNextPage: boolean;
  hasPrevPage: boolean;
  itemsPerPage: number;
  totalItems: number;
  goToPage: (page: number) => void;
  nextPage: () => void;
  prevPage: () => void;
  firstPage: () => void;
  lastPage: () => void;
  setItemsPerPage: (items: number) => void;
}

Server state vs. client state separation

Client-side pagination state includes:

  • Current page number
  • Items per page setting
  • Page navigation actions

Server-side pagination state includes:

  • Total item count (fetched from API)
  • Page size configuration (from API response)
  • Current page index (from API request)

Cache invalidation strategy

When itemsPerPage changes or new data arrives:

  1. Reset current page to 1
  2. Recalculate total pages based on new item count and itemsPerPage
  3. Invalidate any cached data that depends on page state

Optimistic updates design

For client-side pagination, optimistic updates aren't typically needed. However, when integrating with server-side data:

  1. When changing page size, immediately update UI to reflect new itemsPerPage
  2. Fetch next page data asynchronously
  3. Update total pages and current page if needed
  4. Rollback UI if server request fails

Critical-path hook implementation

import { useState, useCallback, useEffect } from 'react';

interface UsePaginationProps {
  initialPage?: number;
  itemsPerPage?: number;
  totalItems?: number;
}

export const usePagination = ({
  initialPage = 1,
  itemsPerPage = 10,
  totalItems = 0
}: UsePaginationProps = {}): PaginationResult => {
  const [currentPage, setCurrentPage] = useState<number>(initialPage);
  const [itemsPerPageState, setItemsPerPageState] = useState<number>(itemsPerPage);
  const [totalItemsState, setTotalItemsState] = useState<number>(totalItems);

  // Calculate total pages
  const totalPages = Math.ceil(totalItemsState / itemsPerPageState);
  
  // Update total items when they change
  useEffect(() => {
    setTotalItemsState(totalItems);
  }, [totalItems]);

  // Handle page navigation
  const goToPage = useCallback((page: number) => {
    if (page >= 1 && page <= totalPages) {
      setCurrentPage(page);
    }
  }, [totalPages]);

  const nextPage = useCallback(() => {
    if (currentPage < totalPages) {
      setCurrentPage(prev => prev + 1);
    }
  }, [currentPage, totalPages]);

  const prevPage = useCallback(() => {
    if (currentPage > 1) {
      setCurrentPage(prev => prev - 1);
    }
  }, [currentPage]);

  const firstPage = useCallback(() => {
    setCurrentPage(1);
  }, []);

  const lastPage = useCallback(() => {
    setCurrentPage(totalPages);
  }, [totalPages]);

  const setItemsPerPage = useCallback((items: number) => {
    setItemsPerPageState(items);
    // Reset to first page when changing items per page
    setCurrentPage(1);
  }, []);

  // Handle edge case where current page exceeds total pages
  useEffect(() => {
    if (currentPage > totalPages && totalPages > 0) {
      setCurrentPage(totalPages);
    }
  }, [totalPages, currentPage]);

  return {
    currentPage,
    totalPages,
    hasNextPage: currentPage < totalPages,
    hasPrevPage: currentPage > 1,
    itemsPerPage: itemsPerPageState,
    totalItems: totalItemsState,
    goToPage,
    nextPage,
    prevPage,
    firstPage,
    lastPage,
    setItemsPerPage
  };
};

// Example usage in a component:
const PaginationComponent = ({ 
  totalItems, 
  itemsPerPage = 10,
  onPageChange
}: {
  totalItems: number;
  itemsPerPage?: number;
  onPageChange?: (page: number) => void;
}) => {
  const {
    currentPage,
    totalPages,
    hasNextPage,
    hasPrevPage,
    goToPage,
    nextPage,
    prevPage,
    firstPage,
    lastPage,
    setItemsPerPage
  } = usePagination({
    initialPage: 1,
    itemsPerPage,
    totalItems
  });

  useEffect(() => {
    onPageChange?.(currentPage);
  }, [currentPage, onPageChange]);

  return (
    <div className="pagination">
      <button 
        onClick={firstPage} 
        disabled={!hasPrevPage}
        aria-label="First page"
      >
        First
      </button>
      <button 
        onClick={prevPage} 
        disabled={!hasPrevPage}
        aria-label="Previous page"
      >
        Previous
      </button>
      
      <span>Page {currentPage} of {totalPages}</span>
      
      <button 
        onClick={nextPage} 
        disabled={!hasNextPage}
        aria-label="Next page"
      >
        Next
      </button>
      <button 
        onClick={lastPage} 
        disabled={!hasNextPage}
        aria-label="Last page"
      >
        Last
      </button>
      
      <select 
        value={itemsPerPage}
        onChange={(e) => setItemsPerPage(Number(e.target.value))}
      >
        <option value={10}>10 per page</option>
        <option value={25}>25 per page</option>
        <option value={50}>50 per page</option>
      </select>
    </div>
  );
};

4. Routing & Navigation

Route Structure and Hierarchy

The pagination component is designed to be a reusable utility that can be integrated into various routes within an application. The typical usage pattern involves:

  • Public Routes: Pagination is used on public listing pages (e.g., /products, /articles)
  • Protected Routes: Pagination is used on authenticated dashboard pages (e.g., /dashboard/users, /admin/orders)
  • Nested Routes: Pagination is embedded within nested route structures (e.g., /category/electronics/products)

The pagination component itself does not define routes but rather integrates with existing route structures through URL parameter management.

graph TD
    A[Root Route] --> B[Public Routes]
    A --> C[Protected Routes]
    B --> D[/products]
    B --> E[/articles]
    C --> F[/dashboard]
    C --> G[/admin]
    F --> H[/dashboard/users]
    G --> I[/admin/orders]

Code Splitting Strategy

Code splitting is implemented at the route level rather than component level for pagination, since it's a utility component:

  1. Route-based code splitting:

    • Public listing pages (/products, /articles) are loaded as separate bundles
    • Protected dashboard pages (/dashboard/users) are loaded as separate bundles
    • Pagination component is bundled with its parent route's code
  2. Component-based code splitting:

    • Pagination component is not split as it's a lightweight utility
    • Only the parent route components are split to reduce initial bundle size

The pagination component itself does not implement navigation guards but respects the authentication context:

  • Authentication Context: Pagination is used within authenticated routes, so it inherits the auth state from the parent route
  • Route Protection: If a user navigates to a protected route without authentication, they are redirected to the login page
  • State Preservation: Pagination state is maintained when navigating between pages within the same route

Deep Linking Support and URL Parameter Design

The pagination component supports deep linking through URL parameters:

// Example URL structure for deep linking
// /products?page=2&limit=10&sort=price&order=asc

interface PaginationParams {
  page: number;
  limit: number;
  sort?: string;
  order?: 'asc' | 'desc';
}

// URL parameter handling in route
const urlParams = new URLSearchParams(window.location.search);
const page = parseInt(urlParams.get('page') || '1', 10);
const limit = parseInt(urlParams.get('limit') || '10', 10);

Browser History Management and Scroll Restoration

The pagination component integrates with browser history management:

  1. History State Updates: When a user navigates to a new page, the URL is updated using history.pushState() or useNavigate from React Router
  2. Scroll Restoration:
    • Built-in browser scroll restoration is used to maintain scroll position
    • Custom scroll restoration logic can be implemented if needed for specific use cases
  3. Back Button Handling:
    • When users press the back button, they return to the previous page state
    • URL parameters are preserved and restored correctly
// Example scroll restoration implementation
useEffect(() => {
  const handlePopState = () => {
    // Restore scroll position or re-fetch data
    window.scrollTo(0, 0);
  };

  window.addEventListener('popstate', handlePopState);
  return () => window.removeEventListener('popstate', handlePopState);
}, []);

The component maintains a clean separation between URL parameters and internal state, ensuring that navigation behaves predictably while preserving user context.

5. API Integration Layer

API client design pattern

We'll implement a custom fetch wrapper with axios as the underlying HTTP client to provide consistent request handling and error management across the application.

// api/client.ts
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';

const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'https://api.example.com';

const createApiClient = (): AxiosInstance => {
  const client = axios.create({
    baseURL: API_BASE_URL,
    timeout: 10000,
    headers: {
      'Content-Type': 'application/json',
    },
  });

  // Request interceptor for adding auth token
  client.interceptors.request.use(
    (config) => {
      const token = localStorage.getItem('authToken');
      if (token) {
        config.headers.Authorization = `Bearer ${token}`;
      }
      return config;
    },
    (error) => Promise.reject(error)
  );

  // Response interceptor for handling errors
  client.interceptors.response.use(
    (response) => response,
    (error) => {
      if (error.response?.status === 401) {
        // Handle unauthorized access
        localStorage.removeItem('authToken');
        window.location.href = '/login';
      }
      return Promise.reject(error);
    }
  );

  return client;
};

export const apiClient = createApiClient();

Request/response interceptors

The API client handles authentication token injection, response normalization, and error transformation to provide a consistent interface for all API calls.

Error handling and retry strategy

We implement a retry mechanism with exponential backoff for transient network errors, while ensuring that client-side errors (4xx) don't retry.

// api/errorHandler.ts
import { AxiosError } from 'axios';

export class ApiError extends Error {
  constructor(
    public status: number,
    public message: string,
    public originalError?: AxiosError
  ) {
    super(message);
    this.name = 'ApiError';
  }
}

export const handleApiError = (error: AxiosError): ApiError => {
  if (error.response) {
    const { status, data } = error.response;
    return new ApiError(
      status,
      data?.message || `API Error: ${status}`,
      error
    );
  }
  
  if (error.request) {
    return new ApiError(
      503,
      'Network error: Unable to reach server',
      error
    );
  }

  return new ApiError(500, 'Unknown error occurred', error);
};

export const shouldRetry = (error: AxiosError): boolean => {
  const status = error.response?.status;
  
  // Retry on server errors (5xx) and network timeouts
  return (
    (status && status >= 500) || 
    error.code === 'ECONNABORTED' ||
    !status
  );
};

Loading, error, and empty states design

We implement a robust state management system that handles all possible API outcomes:

// api/types.ts
export type ApiState<T> = 
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: ApiError };

export type PaginatedResponse<T> = {
  items: T[];
  total: number;
  page: number;
  limit: number;
  totalPages: number;
};

Data fetching strategy

For a pagination component, we use client-side rendering (CSR) with React Query for efficient data fetching and caching:

// hooks/usePaginatedData.ts
import { useQuery, UseQueryResult } from '@tanstack/react-query';
import { apiClient } from '../api/client';
import { PaginatedResponse } from '../api/types';

export const fetchPaginatedData = async (
  endpoint: string,
  page: number,
  limit: number
): Promise<PaginatedResponse<any>> => {
  const response = await apiClient.get<PaginatedResponse<any>>(endpoint, {
    params: { page, limit }
  });
  
  return response.data;
};

export const usePaginatedData = (
  endpoint: string,
  page: number,
  limit: number
): UseQueryResult<PaginatedResponse<any>, ApiError> => {
  return useQuery({
    queryKey: [endpoint, page, limit],
    queryFn: () => fetchPaginatedData(endpoint, page, limit),
    staleTime: 5 * 60 * 1000, // 5 minutes
    cacheTime: 10 * 60 * 1000, // 10 minutes
    retry: shouldRetry,
    retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000),
  });
};

API response transformation layer

We create a consistent data model mapping for all paginated responses:

// api/mappers.ts
import { PaginatedResponse } from './types';

export const mapPaginatedResponse = <T>(
  response: PaginatedResponse<T>
): PaginatedResponse<T> => {
  return {
    items: response.items,
    total: response.total,
    page: response.page,
    limit: response.limit,
    totalPages: response.totalPages
  };
};

Sequence diagram for primary data flows

sequenceDiagram
    participant User
    participant PaginationComponent
    participant ReactQuery
    participant ApiClient
    participant Server

    User->>PaginationComponent: Change page
    PaginationComponent->>ReactQuery: Fetch new data
    ReactQuery->>ApiClient: Make API request
    ApiClient->>Server: GET /api/data?page=2&limit=10
    Server-->>ApiClient: Response with items, total, etc.
    ApiClient-->>ReactQuery: Return parsed data
    ReactQuery-->>PaginationComponent: Update UI with new page

6. Performance Design

Core Web Vitals Targets

  • Largest Contentful Paint (LCP): <2.5s for pagination component
  • Interaction to Next Paint (INP): <200ms for pagination clicks
  • Cumulative Layout Shift (CLS): <0.1 for pagination UI elements

Bundle Size Budget per Route

  • Pagination component: <5KB (minified + gzipped)
  • Pagination-related utilities and hooks: <2KB
  • Dependency overhead (e.g., react-virtual): <1KB

Critical Rendering Path Optimization

  • Pagination component must be rendered within first 100ms of page load for critical paths
  • Use CSS containment (contain: layout style paint) to prevent layout shifts
  • Preload pagination icons and font weights using <link rel="preload"> in HTML head

Image Optimization Strategy

  • Pagination icons (chevrons, dots) are SVGs with inline base64 encoding for zero-RTT loading
  • No image assets required for basic pagination functionality
  • For custom icons, use WebP format with lazy loading via loading="lazy"

Font Loading Strategy

  • Use font-display: swap for pagination text elements (e.g., page numbers)
  • Preload critical font weights using @font-face with font-display: swap
  • Fallback to system fonts for instant rendering during font loading

Memoization Strategy

// Memoize expensive calculations in pagination logic
const memoizedPageRange = useMemo(() => {
  return calculateVisiblePages(currentPage, totalPages, maxVisible);
}, [currentPage, totalPages, maxVisible]);

const handlePageClick = useCallback((page: number) => {
  // Prevent unnecessary re-renders of child components
}, []);

// Use React.memo for pagination item components
const PaginationItem = React.memo(({ page, isActive }: { page: number; isActive: boolean }) => {
  return <button aria-current={isActive ? 'page' : undefined}>{page}</button>;
});

Virtualization for Large Lists

For pagination with >100 pages:

// Use react-window for virtualized page navigation
const VirtualizedPagination = ({ 
  totalPages, 
  currentPage, 
  onPageChange 
}: VirtualizedPaginationProps) => {
  const itemSize = 40; // Fixed height of each page button
  const visibleItems = Math.min(10, totalPages); // Show max 10 items
  
  return (
    <List
      height={itemSize * visibleItems}
      itemCount={totalPages}
      itemSize={itemSize}
      width="100%"
    >
      {({ index, style }) => (
        <div style={style}>
          <PageButton 
            page={index + 1} 
            isActive={index + 1 === currentPage}
            onClick={() => onPageChange(index + 1)}
          />
        </div>
      )}
    </List>
  );
};

Prefetching and Preloading Strategy

  • Preload: Next page content when user hovers over "next" button (if server-side rendering is enabled)
  • Prefetch: Preload page 1 and last page data when user navigates to a large page set
  • Lazy Load: Only render page buttons that are likely to be visible in viewport (with 2-3 pages of buffer)
  • Debounce: Delay API calls when user rapidly navigates through pages (50ms debounce)
// Example prefetching implementation
const usePaginationPrefetch = (currentPage: number, totalPages: number) => {
  useEffect(() => {
    const prefetchPages = [currentPage - 1, currentPage + 1, 1, totalPages];
    prefetchPages.forEach(page => {
      if (page >= 1 && page <= totalPages) {
        // Trigger prefetch for page data
        prefetchPageData(page);
      }
    });
  }, [currentPage, totalPages]);
};

7. Error Handling & Resilience

Error Boundary Placement Strategy

Error boundaries should be strategically placed to provide the best user experience while maintaining system resilience:

  1. Application-level error boundary - Wrapped around entire app or main layout to catch unhandled errors
  2. Pagination container boundary - Wrapped around pagination component to handle internal rendering issues
  3. Data fetching boundary - Wrapped around data display areas that rely on paginated content
graph TD
    A[App] --> B[ErrorBoundary]
    B --> C[MainLayout]
    C --> D[DataDisplay]
    C --> E[PaginationComponent]
    D --> F[DataFetchingErrorBoundary]
    E --> G[PaginationErrorBoundary]

Global Error Handler Design

// src/core/errorHandler.ts
import { ErrorInfo } from 'react';

interface ErrorHandlerConfig {
  reportToSentry?: boolean;
  showUserFriendlyMessage?: boolean;
  logToConsole?: boolean;
}

class GlobalErrorHandler {
  private config: ErrorHandlerConfig;

  constructor(config: ErrorHandlerConfig = {}) {
    this.config = {
      reportToSentry: true,
      showUserFriendlyMessage: true,
      logToConsole: true,
      ...config
    };
  }

  handleError(error: Error, errorInfo: ErrorInfo): void {
    if (this.config.logToConsole) {
      console.error('Unhandled error:', error, errorInfo);
    }

    if (this.config.reportToSentry) {
      // Sentry.captureException(error, { contexts: { react: errorInfo } });
    }

    if (this.config.showUserFriendlyMessage) {
      // Show toast notification or fallback UI
      this.showUserFriendlyError(error);
    }
  }

  private showUserFriendlyError(error: Error): void {
    // Implementation for user-facing error display
  }
}

export const globalErrorHandler = new GlobalErrorHandler();

Offline / Network Failure Handling

// src/hooks/useNetworkStatus.ts
import { useState, useEffect } from 'react';

export const useNetworkStatus = () => {
  const [isOnline, setIsOnline] = useState<boolean>(navigator.onLine);

  useEffect(() => {
    const handleOnline = () => setIsOnline(true);
    const handleOffline = () => setIsOnline(false);

    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);

    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  return isOnline;
};

// src/components/PaginationWithOfflineSupport.tsx
import { useNetworkStatus } from '../hooks/useNetworkStatus';

interface PaginationWithOfflineSupportProps {
  currentPage: number;
  totalPages: number;
  onPageChange: (page: number) => void;
}

export const PaginationWithOfflineSupport = ({
  currentPage,
  totalPages,
  onPageChange
}: PaginationWithOfflineSupportProps) => {
  const isOnline = useNetworkStatus();
  
  if (!isOnline) {
    return (
      <div className="pagination-offline">
        <p>Offline mode - Data is not available</p>
        <button 
          onClick={() => window.location.reload()}
          className="retry-button"
        >
          Retry Connection
        </button>
      </div>
    );
  }

  return <Pagination currentPage={currentPage} totalPages={totalPages} onPageChange={onPageChange} />;
};

Graceful Degradation Patterns

// src/components/PaginationFallback.tsx
export const PaginationFallback = () => {
  return (
    <div className="pagination-fallback">
      <div className="pagination-skeleton">
        <div className="skeleton-button" />
        <div className="skeleton-button" />
        <div className="skeleton-button" />
      </div>
    </div>
  );
};

// src/components/Pagination.tsx
interface PaginationProps {
  currentPage: number;
  totalPages: number;
  onPageChange: (page: number) => void;
  loading?: boolean;
  error?: string | null;
}

export const Pagination = ({
  currentPage,
  totalPages,
  onPageChange,
  loading = false,
  error = null
}: PaginationProps) => {
  if (loading) {
    return <PaginationFallback />;
  }

  if (error) {
    return (
      <div className="pagination-error">
        <p>Error loading pagination: {error}</p>
        <button onClick={() => window.location.reload()}>Retry</button>
      </div>
    );
  }

  // Normal pagination rendering
  return (
    <nav className="pagination" aria-label="Pagination">
      {/* Pagination UI */}
    </nav>
  );
};

User Feedback on Errors

// src/components/ToastManager.tsx
import { useState, useEffect } from 'react';

interface Toast {
  id: string;
  message: string;
  type: 'error' | 'success' | 'warning';
  duration?: number;
}

export const ToastManager = () => {
  const [toasts, setToasts] = useState<Toast[]>([]);

  const addToast = (message: string, type: 'error' | 'success' | 'warning', duration = 5000) => {
    const id = Date.now().toString();
    setToasts(prev => [...prev, { id, message, type, duration }]);
    
    if (type === 'error') {
      // Log to monitoring service
      console.error(`Toast Error: ${message}`);
    }
  };

  const removeToast = (id: string) => {
    setToasts(prev => prev.filter(toast => toast.id !== id));
  };

  useEffect(() => {
    const intervals = toasts.map(toast => 
      setTimeout(() => removeToast(toast.id), toast.duration)
    );

    return () => intervals.forEach(clearTimeout);
  }, [toasts]);

  return (
    <div className="toast-container">
      {toasts.map(toast => (
        <div 
          key={toast.id} 
          className={`toast toast-${toast.type}`}
          role="alert"
        >
          {toast.message}
        </div>
      ))}
    </div>
  );
};

// src/hooks/useToast.ts
import { ToastManager } from '../components/ToastManager';

export const useToast = () => {
  const toast = (message: string, type: 'error' | 'success' | 'warning') => {
    // Implementation to show toast
  };

  return { toast };
};

Error Boundary Component

// src/components/ErrorBoundary.tsx
import { Component, ErrorInfo } from 'react';

interface ErrorBoundaryState {
  hasError: boolean;
  error?: Error;
}

export class ErrorBoundary extends Component<
  { children: React.ReactNode },
  ErrorBoundaryState
> {
  constructor(props: { children: React.ReactNode }) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error: Error): ErrorBoundaryState {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error('ErrorBoundary caught error:', error, errorInfo);
    // Log to monitoring service
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-boundary">
          <h2>Something went wrong</h2>
          <p>We're sorry, but an error occurred while rendering this component.</p>
          <button onClick={() => window.location.reload()}>
            Reload Page
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

Retry Mechanism Implementation

// src/hooks/usePaginationRetry.ts
import { useState, useCallback } from 'react';

interface RetryConfig {
  maxRetries?: number;
  retryDelay?: number;
}

export const usePaginationRetry = (config: RetryConfig = {}) => {
  const { maxRetries = 3, retryDelay = 1000 } = config;
  const [retryCount, setRetryCount] = useState(0);
  const [isLoading, setIsLoading] = useState(false);

  const handleRetry = useCallback(async (retryFn: () => Promise<void>) => {
    if (retryCount >= maxRetries) {
      throw new Error(`Max retries (${maxRetries}) exceeded`);
    }

    setIsLoading(true);
    
    try {
      await retryFn();
      setRetryCount(0);
    } catch (error) {
      setRetryCount(prev => prev + 1);
      setTimeout(() => handleRetry(retryFn), retryDelay * Math.pow(2, retryCount));
      throw error;
    } finally {
      setIsLoading(false);
    }
  }, [retryCount, maxRetries, retryDelay]);

  return { handleRetry, retryCount, isLoading };
};

Comprehensive Error Handling Strategy

  1. Network Errors: Handle 404, 500, timeout responses gracefully
  2. Data Validation Errors: Validate page numbers and handle edge cases (negative pages, out-of-bounds)
  3. Component Errors: Use React error boundaries for graceful degradation
  4. User Experience: Provide clear feedback and retry options without breaking user flow
  5. Monitoring: Log errors to monitoring services with context for debugging
  6. Fallbacks: Implement skeleton loading states and basic UI fallbacks

The error handling strategy prioritizes user experience by providing clear feedback, enabling recovery actions, and maintaining application stability even when individual components or data fetches fail.

8. Testing Strategy

Unit Testing Approach

Unit tests will focus on component behavior, prop validation, and state transitions. Target 85% coverage for core pagination logic with Jest and React Testing Library.

Component Tests:

  • Render with different page counts and current pages
  • Validate button states (disabled/enabled)
  • Test click handlers for navigation
  • Verify accessibility attributes
  • Test edge cases (first/last page, single page)

Utility Tests:

  • Pagination calculation logic
  • Boundary handling (empty states, overflow)
  • Page range generation algorithms

Integration Testing Strategy

Test the pagination component in context with:

  • Data fetching hooks (React Query integration)
  • Route navigation flows
  • State management interactions
  • Error state transitions

E2E Testing Scope

End-to-end tests will cover:

  • Full user journey: fetch data → render pagination → navigate pages → handle errors
  • Keyboard navigation through page controls
  • Responsive behavior across breakpoints
  • Server-side rendering scenarios

Tools: Playwright for E2E testing with cross-browser support (Chrome, Firefox, Safari)

Visual Regression Testing

Implement visual regression testing using Percy or Chromatic to:

  • Capture component states (first, last, middle pages)
  • Test responsive layouts across device sizes
  • Validate accessibility contrast compliance
  • Monitor UI changes in CI/CD pipeline

Accessibility Testing Automation

Automate ARIA compliance using axe-core with:

  • Keyboard navigation flow validation
  • Screen reader announcement testing
  • Focus order verification
  • Color contrast ratio checks

Test Data Strategy

Create comprehensive test fixtures:

  • Empty dataset (0 items)
  • Single page dataset (< itemsPerPage)
  • Multi-page dataset (≥ itemsPerPage)
  • Edge case scenarios (1 item, 1000 items)
  • Error response mock data

Sample test structure:

// Pagination.test.tsx
describe('Pagination Component', () => {
  const mockProps = {
    currentPage: 1,
    totalPages: 5,
    onPageChange: jest.fn(),
  };

  it('renders correct number of page buttons', () => {
    const { container } = render(<Pagination {...mockProps} />);
    expect(container.querySelectorAll('button')).toHaveLength(7); // prev, 1-3, next
  });

  it('disables previous button on first page', () => {
    const { getByLabelText } = render(
      <Pagination {...mockProps} currentPage={1} />
    );
    expect(getByLabelText('Previous page')).toBeDisabled();
  });

  it('handles page change correctly', () => {
    const onPageChange = jest.fn();
    const { getByText } = render(
      <Pagination {...mockProps} onPageChange={onPageChange} />
    );
    
    fireEvent.click(getByText('2'));
    expect(onPageChange).toHaveBeenCalledWith(2);
  });
});

9. Accessibility (A11y)

WCAG 2.1 AA compliance checklist for this feature

The pagination component must meet the following WCAG 2.1 AA requirements:

  • 1.1.1 Non-text Content: All pagination controls must have text alternatives (e.g., "Previous page", "Next page", "Go to page 5")
  • 1.3.1 Info and Relationships: Proper semantic HTML structure (nav, ul, li) to convey the relationship between pagination items
  • 1.4.1 Use of Color: Sufficient color contrast (4.5:1) between text and background for all pagination states
  • 1.4.3 Contrast (Minimum): Text and interactive elements must maintain minimum 4.5:1 contrast ratio
  • 2.1.1 Keyboard: All pagination functionality must be operable via keyboard navigation only
  • 2.4.1 Bypass Blocks: Pagination should be navigable without tabbing through all items (skip links or proper heading structure)
  • 2.4.3 Focus Order: Logical tab order that matches visual reading order
  • 2.4.6 Headings and Labels: Clear labels for each page control (Previous, Next, Page numbers)
  • 3.1.1 Language: Proper language attribute if content is in different languages
  • 4.1.2 Name, Role, Value: All interactive elements must have proper ARIA roles and properties

Keyboard navigation support

The pagination component supports full keyboard navigation:

  • Tab key: Navigates between pagination controls in logical order (First, Previous, Page numbers, Next, Last)
  • Arrow keys:
    • Left/Right arrows navigate between page numbers when focused on a page number
    • Up/Down arrows move focus between pagination controls in the same group
  • Enter/Space: Activates selected page control or navigates to a specific page when focused on a page number
  • Home/End: Jump to first or last page control in the navigation group

Focus management strategy

Focus management is implemented with:

  • Focus trapping: When the pagination container receives focus, keyboard navigation is trapped within its controls
  • Focus restoration: When a page changes (e.g., via navigation), focus returns to the newly active page number or first control in the new view
  • Focus indicators: Clear visual focus states that are visible to users with low vision or those who rely on keyboard navigation
  • Focus order preservation: Maintains logical reading order that matches the DOM structure

ARIA roles and attributes plan

// Pagination component ARIA implementation
interface PaginationProps {
  currentPage: number;
  totalPages: number;
  onPageChange: (page: number) => void;
  ariaLabel?: string;
}

const Pagination = ({
  currentPage,
  totalPages,
  onPageChange,
  ariaLabel = "Pagination navigation"
}: PaginationProps) => {
  // ARIA roles and properties for accessibility
  return (
    <nav 
      role="navigation" 
      aria-label={ariaLabel}
      aria-current={`page ${currentPage}`}
    >
      <ul role="menubar" className="pagination-list">
        <li role="none">
          <button
            role="menuitem"
            aria-label="First page"
            aria-disabled={currentPage === 1}
            onClick={() => onPageChange(1)}
            disabled={currentPage === 1}
          >
            First
          </button>
        </li>
        <li role="none">
          <button
            role="menuitem"
            aria-label="Previous page"
            aria-disabled={currentPage === 1}
            onClick={() => onPageChange(currentPage - 1)}
            disabled={currentPage === 1}
          >
            Previous
          </button>
        </li>
        {renderPageNumbers()}
        <li role="none">
          <button
            role="menuitem"
            aria-label="Next page"
            aria-disabled={currentPage === totalPages}
            onClick={() => onPageChange(currentPage + 1)}
            disabled={currentPage === totalPages}
          >
            Next
          </button>
        </li>
        <li role="none">
          <button
            role="menuitem"
            aria-label="Last page"
            aria-disabled={currentPage === totalPages}
            onClick={() => onPageChange(totalPages)}
            disabled={currentPage === totalPages}
          >
            Last
          </button>
        </li>
      </ul>
    </nav>
  );
};

Screen reader compatibility

The component is designed to work with screen readers:

  • Semantic HTML: Uses proper <nav>, <ul>, <li>, and <button> elements
  • Descriptive labels: All interactive elements have descriptive aria-label attributes
  • Live regions: When the current page changes, screen readers announce the new page number
  • Dynamic content updates: Uses aria-live="polite" for status updates when page changes
  • Role hierarchy: Proper ARIA role hierarchy that screen readers can understand (navigation, menubar, menuitem)
stateDiagram-v2
    [*] --> Idle
    Idle --> ActivePage: Focus on page number
    ActivePage --> Idle: Blur or navigate away
    ActivePage --> NextPage: Arrow Right
    ActivePage --> PrevPage: Arrow Left
    Idle --> FirstPage: Home key
    Idle --> LastPage: End key
    NextPage --> ActivePage: Focus on next page
    PrevPage --> ActivePage: Focus on previous page
    
    [*] --> DisabledState
    DisabledState --> Idle: Page becomes active

The state machine shows how focus moves between different pagination states and ensures that screen readers can properly announce the current page state and navigation options.

10. Security Considerations

XSS Prevention Strategy

The pagination component must prevent cross-site scripting attacks through proper input sanitization and output encoding:

// Pagination component with XSS protection
const PaginationItem = ({ page, isActive, onClick }: PaginationItemProps) => {
  // Sanitize page number to prevent XSS
  const sanitizedPage = isNaN(page) ? 1 : Math.max(1, page);
  
  return (
    <button
      aria-current={isActive ? 'page' : undefined}
      onClick={() => onClick(sanitizedPage)}
      // Ensure all dynamic content is properly escaped
      dangerouslySetInnerHTML={{ __html: sanitizeHTML(page.toString()) }}
    >
      {page}
    </button>
  );
};

// Helper function to sanitize HTML content
const sanitizeHTML = (html: string): string => {
  const div = document.createElement('div');
  div.textContent = html;
  return div.innerHTML;
};

CSRF Protection

When pagination is used in forms or triggers API calls, implement CSRF token handling:

// Pagination with CSRF protection
const PaginatedList = () => {
  const [csrfToken, setCsrfToken] = useState<string>('');
  
  useEffect(() => {
    // Fetch CSRF token from server
    fetch('/api/csrf-token')
      .then(res => res.json())
      .then(data => setCsrfToken(data.token));
  }, []);

  const handlePageChange = (page: number) => {
    // Include CSRF token in all pagination requests
    fetch(`/api/data?page=${page}`, {
      method: 'GET',
      headers: {
        'X-CSRF-Token': csrfToken,
        'Content-Type': 'application/json'
      }
    });
  };

  return <Pagination onPageChange={handlePageChange} />;
};

Content Security Policy (CSP) Headers

Ensure the application's CSP headers properly handle pagination-related content:

Content-Security-Policy: 
  default-src 'self'; 
  script-src 'self' 'unsafe-inline' 'unsafe-eval'; 
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: blob:;

Sensitive Data Handling in Frontend State

Pagination state should never expose sensitive information:

// Secure pagination state management
interface PaginationState {
  currentPage: number;
  totalPages: number;
  itemsPerPage: number;
  // Never store sensitive data in pagination state
  // sensitiveData: string[]; // ❌ Avoid this
}

const useSecurePagination = () => {
  const [pagination, setPagination] = useState<PaginationState>({
    currentPage: 1,
    totalPages: 0,
    itemsPerPage: 10
    // No sensitive fields here
  });

  return {
    ...pagination,
    setCurrentPage: (page: number) => {
      // Validate page is within bounds
      const validatedPage = Math.max(1, Math.min(page, pagination.totalPages));
      setPagination(prev => ({ ...prev, currentPage: validatedPage }));
    }
  };
};

Input Sanitization Approach

All pagination parameters must be validated and sanitized:

// Pagination input validation
const validatePaginationParams = (params: any): ValidatedPaginationParams => {
  const page = parseInt(params.page, 10);
  const limit = parseInt(params.limit, 10);
  
  // Validate and sanitize numeric inputs
  if (isNaN(page) || page < 1) {
    throw new Error('Invalid page parameter');
  }
  
  if (isNaN(limit) || limit < 1 || limit > 100) {
    throw new Error('Invalid limit parameter');
  }
  
  return {
    page: Math.max(1, page),
    limit: Math.min(100, Math.max(1, limit))
  };
};

// Usage in pagination component
const handlePageChange = (page: number) => {
  try {
    const validatedParams = validatePaginationParams({ page, limit: 10 });
    // Safe to use validated parameters
    fetchData(validatedParams.page, validatedParams.limit);
  } catch (error) {
    console.error('Pagination parameter validation failed:', error);
    // Handle invalid input gracefully
  }
};

Security Headers Implementation

Implement security headers for pagination-related API endpoints:

// Server-side security headers for pagination endpoints
const securePaginationRoute = (req, res) => {
  // Set security headers
  res.set({
    'X-Content-Type-Options': 'nosniff',
    'X-Frame-Options': 'DENY',
    'X-XSS-Protection': '1; mode=block',
    'Strict-Transport-Security': 'max-age=31536000; includeSubDomains'
  });
  
  // Validate pagination parameters on server side
  const page = parseInt(req.query.page, 10);
  if (isNaN(page) || page < 1 || page > 1000) {
    return res.status(400).json({ error: 'Invalid page parameter' });
  }
  
  // Process pagination safely
  return res.json({ data: [], currentPage: page });
};

Rate Limiting Considerations

Implement rate limiting to prevent abuse of pagination endpoints:

// Pagination rate limiting middleware (server-side)
const rateLimitPagination = (req, res, next) => {
  const clientIP = req.ip || req.connection.remoteAddress;
  
  // Track pagination requests per IP
  const requestCount = paginationRequestCounts.get(clientIP) || 0;
  
  if (requestCount > 100) { // Max 100 requests per minute
    return res.status(429).json({ 
      error: 'Rate limit exceeded for pagination' 
    });
  }
  
  paginationRequestCounts.set(clientIP, requestCount + 1);
  setTimeout(() => {
    paginationRequestCounts.delete(clientIP);
  }, 60000); // Reset after 1 minute
  
  next();
};

Secure URL Parameter Handling

Ensure pagination parameters in URLs are properly validated:

// URL parameter validation for pagination
const validatePageFromURL = (url: string): number => {
  const urlObj = new URL(url);
  const pageParam = urlObj.searchParams.get('page');
  
  if (!pageParam) return 1;
  
  const page = parseInt(pageParam, 10);
  
  // Validate page number range
  if (isNaN(page) || page < 1 || page > 10000) {
    throw new Error('Invalid page number in URL');
  }
  
  return page;
};

// Safe URL generation for pagination
const generatePageURL = (page: number, baseUrl: string): string => {
  const url = new URL(baseUrl);
  url.searchParams.set('page', page.toString());
  
  // Ensure no malicious parameters are added
  const allowedParams = ['page', 'limit', 'sort'];
  for (const [key, value] of url.searchParams.entries()) {
    if (!allowedParams.includes(key)) {
      url.searchParams.delete(key);
    }
  }
  
  return url.toString();
};

11. Internationalization (i18n)

Translation Key Structure and Naming Convention

Translation keys will follow a hierarchical structure using dot notation for clarity and maintainability:

// Translation key structure example:
{
  "pagination": {
    "page": "Page {{currentPage}} of {{totalPages}}",
    "itemsPerPage": "{{count}} items per page",
    "firstPage": "First Page",
    "lastPage": "Last Page",
    "nextPage": "Next Page",
    "previousPage": "Previous Page",
    "goToPage": "Go to page {{page}}",
    "showing": "Showing {{start}} to {{end}} of {{total}} results"
  }
}

RTL Support Considerations

RTL (Right-to-Left) language support will be handled through CSS classes and direction properties:

/* RTL-aware pagination styling */
.pagination {
  direction: ltr;
  text-align: left;
}

[dir="rtl"] .pagination {
  direction: rtl;
  text-align: right;
}

.pagination-button {
  margin-left: 8px;
  margin-right: 8px;
}

Date, Number, Currency Formatting Strategy

For displaying page numbers and item counts, we'll use the Intl.NumberFormat API to handle locale-specific formatting:

const formatNumber = (num: number, locale: string = 'en-US') => {
  return new Intl.NumberFormat(locale).format(num);
};

// Example usage in component:
const pageText = t('pagination.page', {
  currentPage: formatNumber(currentPage),
  totalPages: formatNumber(totalPages)
});

Locale Detection and Switching Strategy

The i18n system will support automatic locale detection from browser settings and manual switching:

// i18n context implementation
interface I18nContextType {
  locale: string;
  setLocale: (locale: string) => void;
  t: (key: string, params?: Record<string, unknown>) => string;
}

// Automatic detection logic
const detectLocale = (): string => {
  const browserLang = navigator.language || 'en-US';
  const supportedLocales = ['en-US', 'fr-FR', 'es-ES', 'ar-SA'];
  
  // Match exact or language-only
  const matched = supportedLocales.find(
    locale => 
      locale === browserLang || 
      locale.startsWith(browserLang.split('-')[0])
  );
  
  return matched || 'en-US';
};

// Manual switching with persistence
const switchLocale = (newLocale: string) => {
  localStorage.setItem('preferredLocale', newLocale);
  i18nContext.setLocale(newLocale);
};

The pagination component will respect the current locale for displaying page numbers and text, with proper handling of RTL layouts where needed. All numeric displays will use locale-appropriate formatting to ensure accessibility for users in different regions.

12. Monitoring & Observability

Real User Monitoring (RUM) Metrics to Track

The pagination component should capture key performance indicators to understand user behavior and system responsiveness:

  • Page Load Time: Time from initial request to complete render of pagination controls
  • Click Response Time: Latency between user click and page content update
  • Scroll Performance: For infinite scroll implementations, measure smoothness and frame drops
  • Error Rate: Percentage of failed pagination requests or UI errors
  • User Engagement: Click frequency on pagination controls, page transitions

Error Tracking Integration

Integrate with error monitoring tools like Sentry or Datadog to capture:

  • API Failure Events: When pagination API calls fail (4xx/5xx responses)
  • Client-side Errors: React error boundaries catching pagination component crashes
  • Missing Data States: When pagination returns empty results unexpectedly
  • Invalid State Transitions: Invalid page number transitions or boundary violations

Custom Events and User Journey Tracking

Implement custom event tracking for key pagination interactions:

// Example event tracking structure
const trackPaginationEvent = (event: {
  action: 'page_change' | 'first_page' | 'last_page' | 'next_page' | 'prev_page';
  page: number;
  total: number;
  timestamp: number;
  userId?: string;
}) => {
  // Send to analytics service (e.g., Mixpanel, Amplitude)
  analytics.track('pagination_interaction', event);
};

Performance Monitoring and Regression Detection

  • Bundle Size Tracking: Monitor pagination component bundle size changes over time
  • Memory Usage: Track memory consumption during large dataset pagination
  • Rendering Performance: Measure React re-render frequency and identify bottlenecks
  • API Response Time: Track pagination API latency trends to detect regressions

Implementation Strategy

// Performance monitoring hook for pagination
const usePaginationMonitoring = () => {
  const [metrics, setMetrics] = useState({
    pageLoadTime: 0,
    apiCallDuration: 0,
    errorCount: 0
  });

  const trackPageLoad = (duration: number) => {
    setMetrics(prev => ({ ...prev, pageLoadTime: duration }));
    // Send to performance monitoring service
    performanceMonitor.record('pagination_page_load', { duration });
  };

  const trackApiCall = (duration: number) => {
    setMetrics(prev => ({ ...prev, apiCallDuration: duration }));
    // Send to performance monitoring service
    performanceMonitor.record('pagination_api_call', { duration });
  };

  return { metrics, trackPageLoad, trackApiCall };
};

This monitoring approach enables proactive detection of performance issues and provides insights into how users interact with pagination, supporting data-driven improvements to the component's behavior and reliability.

13. File & Folder Structure

Proposed Directory Structure

src/
├── components/
│   ├── Pagination/
│   │   ├── Pagination.tsx
│   │   ├── PaginationButton.tsx
│   │   ├── usePagination.ts
│   │   └── index.ts
├── features/
│   └── ProductList/
│       ├── ProductList.tsx
│       ├── ProductItem.tsx
│       └── index.ts
├── hooks/
│   └── usePagination.ts
└── types/
    └── pagination.ts

Module Boundaries and Import Rules

  • Pagination component is self-contained and should not import directly from other features
  • Shared hooks like usePagination can be imported by any component that needs pagination logic
  • Feature-specific components (like ProductList) should import Pagination as a complete unit from components/Pagination
  • No cross-feature imports within the same module level (e.g., ProductList shouldn't import from UserList)

Shared vs Feature-Specific Code Separation

Shared Pagination Component:

  • Contains all pagination logic and UI elements
  • Reusable across different data lists (products, users, etc.)
  • Implements accessibility and internationalization patterns

Feature-Specific Integration:

  • Feature components (ProductList, UserList) consume the shared Pagination component
  • Handle feature-specific data fetching and state management
  • Implement feature-specific styling and theming

The structure follows a feature-based organization where each feature owns its components, but shared logic is extracted into reusable modules that can be consumed by multiple features. This approach maintains clear module boundaries while enabling code reuse and consistent behavior across the application.

14. Dependencies & Third-Party Libraries

Key Libraries Selected with Justification

1. React (v18+)

  • Justification: Core library for building UI components with hooks, context, and concurrent rendering capabilities
  • Bundle impact: ~150KB (minified)
  • Rationale: Required for all React-based component development and ecosystem compatibility

2. TypeScript (v5+)

  • Justification: Type safety for component contracts, state management, and API integration
  • Bundle impact: None (compile-time only)
  • Rationale: Essential for maintaining code quality and preventing runtime errors in large-scale applications

3. react-window (v1.8+)

  • Justification: Virtualization for large datasets to maintain performance when rendering thousands of items
  • Bundle impact: ~20KB (minified)
  • Rationale: Critical for maintaining good LCP and INP when displaying large lists with pagination

4. react-query (v5+)

  • Justification: Server state management for pagination API calls with caching, invalidation, and optimistic updates
  • Bundle impact: ~25KB (minified)
  • Rationale: Provides robust data fetching patterns with automatic refetching and cache management

5. clsx (v2+)

  • Justification: Class name composition utility for conditional styling in a clean, maintainable way
  • Bundle impact: ~1KB (minified)
  • Rationale: Reduces CSS class management complexity and improves readability

6. @radix-ui/react-slot (v1+)

  • Justification: Enables component composition patterns for flexible, extensible pagination components
  • Bundle impact: ~2KB (minified)
  • Rationale: Allows consumers to customize pagination elements while maintaining semantic structure

Alternatives Considered and Rejected

1. react-virtualized

  • Rejected: Outdated and not actively maintained; lacks React 18 support
  • Rationale: Would require additional migration effort and lacks modern React features

2. Redux Toolkit

  • Rejected: Overkill for pagination state management; react-query handles server state better
  • Rationale: Pagination is primarily client-side rendering concern with minimal global state

3. Material-UI (MUI) Pagination

  • Rejected: Too opinionated for custom design systems; would require extensive theming overrides
  • Rationale: Custom implementation allows better alignment with existing design language and accessibility requirements

4. react-infinite-scroll-component

  • Rejected: Not designed for pagination with discrete page boundaries; better suited for infinite scroll
  • Rationale: Pagination requires explicit page transitions, not continuous loading

Licensing and Bundle Size Impact Assessment

LibraryLicenseBundle Size (minified)Notes
ReactMIT~150KBCore dependency
TypeScriptApache 2.0NoneCompile-time only
react-windowMIT~20KBPerformance-critical
react-queryMIT~25KBServer state management
clsxMIT~1KBUtility function
@radix-ui/react-slotMIT~2KBComponent composition

Total estimated bundle size impact: ~198KB (excluding React core) Bundle optimization strategy: Tree-shaking and route-based code splitting to minimize impact on initial load

Performance Considerations

Bundle size budget:

  • Pagination component bundle: < 50KB (post-minification, post-Gzip)
  • Critical path dependencies: < 180KB total (excluding React core)

Code splitting strategy:

  • Pagination component is self-contained and can be lazy-loaded when needed
  • react-window and react-query are shared dependencies that will be bundled with other feature modules

Tree-shaking compatibility:

  • All selected libraries support tree-shaking for minimal bundle inclusion
  • Only required modules are imported to reduce final bundle size

15. Open Questions, Risks & Trade-offs

Decisions that need product/engineering alignment

  • Pagination strategy: Should we implement client-side pagination for small datasets or always use server-side pagination? This decision impacts performance, API load, and UX consistency.
  • Page size configuration: Should users be able to customize page sizes (e.g., 10, 25, 50 items per page) or should we enforce fixed sizes? This affects both UI complexity and API contract requirements.
  • Infinite scroll vs. traditional pagination: Should we support infinite scroll as an alternative or complementary interaction pattern? This impacts the component's flexibility and user experience.

Known trade-offs made in this design

  • Component complexity vs. reusability: The pagination component is designed to be highly configurable but this increases the API surface area and potential for misuse. Simpler, more opinionated components might be more maintainable in the long run.
  • Server vs. client state management: We're leaning toward server-side pagination for better scalability, but this introduces more complex API integration and state management overhead compared to client-side solutions.
  • Accessibility vs. performance: Comprehensive ARIA implementation and keyboard navigation support add complexity to the component but are essential for accessibility compliance. The trade-off is between performance and inclusive design.

Risks and mitigations

  • API contract mismatch risk: If the backend pagination API changes (e.g., different query parameters or response format), our component will break. Mitigation: Implement robust error handling and maintain a clear API contract specification.
  • Performance degradation with large datasets: If the page count is very high, rendering all pagination controls can impact performance. Mitigation: Implement virtualization for page numbers and lazy loading of page content.
  • Memory leaks in long-running applications: If pagination is used across multiple routes without proper cleanup, memory can accumulate. Mitigation: Ensure proper cleanup of event listeners and subscriptions in component lifecycle methods.

Technical debt acknowledged

  • Hardcoded page size assumptions: The current implementation assumes a default page size of 20 items, which may not scale well for all use cases. This creates technical debt that requires future refactoring to support dynamic page sizes.
  • Limited customization options: While the component is flexible, it doesn't yet support advanced UI patterns like compact mode or custom page number formatting. These limitations will require future enhancements to meet diverse design system requirements.
  • No built-in loading states: The component assumes that page data is already loaded, but in real-world applications, there should be proper loading states for the current page content. This is a design gap that needs to be addressed in future iterations.

The design prioritizes correctness and maintainability over maximum flexibility, acknowledging that some edge cases and advanced use patterns will require future enhancements.

Was this article helpful?

Topics in this article

More from Frontend LLD

  • Frontend LLD

    Tooltip Component Design and Implementation

    Comprehensive low-level design for a reusable, accessible tooltip component with dynamic positioning and performance optimizations

  • Frontend LLD

    Calendar Component Design System

    Comprehensive low-level design for a scalable, accessible calendar component with recurring events and timezone support

Stay in the Loop

Get curated DSA patterns, coding problems, frontend and backend LLD/HLD, and AI/ML System Design insights delivered straight to your inbox. No spam, just focused interview and engineering content.