Custom Pagination with TanStack Query: A Production Lever

In today's fast-paced product environments, delivering data in a responsive and scalable manner is paramount. For companies that need to serve thousands of users, efficient data fetching and rendering are critical. TanStack Query (formerly React Query) provides a robust solution to manage server state, and when combined with custom pagination logic, it allows for an optimized user experience and improved system performance. This approach not only reduces server load but also enables precise control over data flow a significant lever in production systems. Setting Up TanStack Query and Developer Tools To get started, the integration of TanStack Query and its dev tools is straightforward: # Install the necessary packages npm install @tanstack/react-query npm install @tanstack/react-query-devtools // Initialize QueryClient at the root of your application import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; const queryClient = new QueryClient(); function App() { return ( ); } export default App; This snippet sets the foundation for using TanStack Query, ensuring that the QueryClient is available throughout the app, and provides dev tools to monitor query statuses during development and production troubleshooting. Custom Hook for Fetching Product Data Moving beyond basic setups, we introduce a custom hook that fetches product details. The hook includes pagination logic that supports server-side filtering and caching. import { ProductStatus } from "@/lib/features/listing/listingSlice"; import { Product, ProductResponse } from "@/types/product/product"; import { Api } from "@/utils/api/api"; import { getUserId } from "@/utils/get-logged-profile-id"; // Updated utility for product context import { useQuery } from "@tanstack/react-query"; import { useState } from "react"; // Custom hook to fetch product details export const useFetchProductDetails = (status?: string) => { const [page, setPageAction] = useState(1); // Current page state const limit = 4; // Items per page, adjustable based on production needs // Construct URL dynamically based on product status and pagination state const URL = `${Api}/product-detail?status=${status}&limit=${limit}&page=${page}` // Fetch product details with TanStack Query const { isPending, error, data, isFetching } = useQuery({ queryKey: ["productDetail", page], queryFn: () => fetch(URL).then((res) => res.json()), }); const userId = getUserId(); // Retrieve user identifier in a product context // Total pages from API response, critical for pagination UI control const totalPages = data?.res?.pagination?.totalPages ?? 0; return { isPending, error, data, isFetching, page, setPageAction, totalPages, }; }; By encapsulating the data fetching logic into a custom hook, we isolate concerns and make the component more testable and maintainable. The filtering by user ensures that only relevant product details are displayed, a necessity in enterprise applications. Implementing Custom Pagination A robust pagination component is key to managing large data sets. Below is an advanced implementation that dynamically calculates page ranges and handles edge cases like gaps in the pagination sequence: import ArrowRightIcon from "@/components/icons/arrow-right-icon"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import React from "react"; import ArrowLeftIcon from "@/components/icons/arrow-left-icon"; interface PaginationProps { currentPage: number; totalPages: number; onPageChange: (page: number) => void; } // Helper function to create a range of numbers function range(start: number, end: number): number[] { return Array.from({ length: end - start + 1 }, (_, i) => start + i); } /** * Returns an array of pages and/or "..." placeholders. * Example: [1, 2, 3, 4, "...", 20] */ function getPaginationRange( currentPage: number, totalPages: number, siblingCount = 1, ): (number | string)[] { const totalPageNumbers = 2 /* first & last */ + 2 * siblingCount + 1; if (totalPages 2; // Indicates a gap on the left const showRightDots = rightSiblingIndex < totalPages - 1; // Indicates a gap on the right const firstPageIndex = 1; const lastPageIndex = totalPages; // 1) No left dots but right dots if (!showLeftDots && showRightDots) { const leftRange = range(1, 3 + 2 * siblingCount); return [...leftRange, "...", lastPageIndex]; } // 2) Left dots but no right dots if (showLeftDots && !showRightDots) { const rightRange = range( totalPages - (3 + 2 * siblingCount) + 1, totalPages, ); return [firstPageIndex, "...", ...rightRange]; } // 3) Both left and right dots const middleRange = range(leftSiblingIndex, rightSiblingIndex); return [firstPageIndex,

Apr 6, 2025 - 19:13
 0
Custom Pagination with TanStack Query: A Production Lever

In today's fast-paced product environments, delivering data in a responsive and scalable manner is paramount. For companies that need to serve thousands of users, efficient data fetching and rendering are critical. TanStack Query (formerly React Query) provides a robust solution to manage server state, and when combined with custom pagination logic, it allows for an optimized user experience and improved system performance. This approach not only reduces server load but also enables precise control over data flow a significant lever in production systems.

Setting Up TanStack Query and Developer Tools

To get started, the integration of TanStack Query and its dev tools is straightforward:

# Install the necessary packages
npm install @tanstack/react-query
npm install @tanstack/react-query-devtools
// Initialize QueryClient at the root of your application
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

const queryClient = new QueryClient();

function App() {
  return (
    
      
      
    
  );
}

export default App;

This snippet sets the foundation for using TanStack Query, ensuring that the QueryClient is available throughout the app, and provides dev tools to monitor query statuses during development and production troubleshooting.

Custom Hook for Fetching Product Data

Moving beyond basic setups, we introduce a custom hook that fetches product details. The hook includes pagination logic that supports server-side filtering and caching.

import { ProductStatus } from "@/lib/features/listing/listingSlice";
import { Product, ProductResponse } from "@/types/product/product";
import { Api } from "@/utils/api/api";
import { getUserId } from "@/utils/get-logged-profile-id"; // Updated utility for product context
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";

// Custom hook to fetch product details
export const useFetchProductDetails = (status?: string) => {
  const [page, setPageAction] = useState(1); // Current page state
  const limit = 4; // Items per page, adjustable based on production needs

  // Construct URL dynamically based on product status and pagination state
  const URL = `${Api}/product-detail?status=${status}&limit=${limit}&page=${page}`

  // Fetch product details with TanStack Query
  const { isPending, error, data, isFetching } = useQuery({
    queryKey: ["productDetail", page],
    queryFn: () => fetch(URL).then((res) => res.json()),
  });

  const userId = getUserId(); // Retrieve user identifier in a product context

  // Total pages from API response, critical for pagination UI control
  const totalPages = data?.res?.pagination?.totalPages ?? 0;

  return {
    isPending,
    error,
    data,
    isFetching,
    page,
    setPageAction,
    totalPages,
  };
};

By encapsulating the data fetching logic into a custom hook, we isolate concerns and make the component more testable and maintainable. The filtering by user ensures that only relevant product details are displayed, a necessity in enterprise applications.

Implementing Custom Pagination

A robust pagination component is key to managing large data sets. Below is an advanced implementation that dynamically calculates page ranges and handles edge cases like gaps in the pagination sequence:

import ArrowRightIcon from "@/components/icons/arrow-right-icon";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import React from "react";
import ArrowLeftIcon from "@/components/icons/arrow-left-icon";

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

// Helper function to create a range of numbers
function range(start: number, end: number): number[] {
  return Array.from({ length: end - start + 1 }, (_, i) => start + i);
}

/**
 * Returns an array of pages and/or "..." placeholders.
 * Example: [1, 2, 3, 4, "...", 20]
 */
function getPaginationRange(
  currentPage: number,
  totalPages: number,
  siblingCount = 1,
): (number | string)[] {
  const totalPageNumbers = 2 /* first & last */ + 2 * siblingCount + 1;
  if (totalPages <= totalPageNumbers) {
    return range(1, totalPages);
  }

  const leftSiblingIndex = Math.max(currentPage - siblingCount, 1);
  const rightSiblingIndex = Math.min(currentPage + siblingCount, totalPages);

  const showLeftDots = leftSiblingIndex > 2; // Indicates a gap on the left
  const showRightDots = rightSiblingIndex < totalPages - 1; // Indicates a gap on the right

  const firstPageIndex = 1;
  const lastPageIndex = totalPages;

  // 1) No left dots but right dots
  if (!showLeftDots && showRightDots) {
    const leftRange = range(1, 3 + 2 * siblingCount);
    return [...leftRange, "...", lastPageIndex];
  }
  // 2) Left dots but no right dots
  if (showLeftDots && !showRightDots) {
    const rightRange = range(
      totalPages - (3 + 2 * siblingCount) + 1,
      totalPages,
    );
    return [firstPageIndex, "...", ...rightRange];
  }
  // 3) Both left and right dots
  const middleRange = range(leftSiblingIndex, rightSiblingIndex);
  return [firstPageIndex, "...", ...middleRange, "...", lastPageIndex];
}

export function Pagination({
  currentPage,
  totalPages,
  onPageChange,
}: PaginationProps) {
  // Build the pagination sequence using the helper function
  const paginationRange = getPaginationRange(currentPage, totalPages, 1);

  return (
    
{/* Render page buttons or ellipses based on computed range */} {paginationRange.map((pageOrDots, idx) => { if (pageOrDots === "...") { return ( ... ); } const pageNumber = pageOrDots as number; return ( ); })}
); }

The dynamic pagination logic especially the calculation of sibling ranges and insertion of ellipses ensures that the UI remains uncluttered, regardless of the total number of pages. This is a best practice in production systems where scalability is a concern.

Integrating Pagination into the Product Page

Finally, the product page ties the custom hook and pagination component together. This high-level component fetches product data and renders both the list and the pagination controls.

"use client";

import UserProductListing from "@/components/sections/user/user-product-listing"; // Updated component for products
import { useFetchProductDetails } from "@/hooks/use-fetch-product-details";
import React from "react";

const Page = () => {
  // Placeholder function to handle additional product interactions
  const handleShowResults = () => {
    // Custom logic can be implemented here
  };

  const { isPending, data: productData, page, setPageAction, totalPages } =
    useFetchProductDetails("published"); // Fetch published products
  const productDetails = productData?.res?.products || [];

  return (
     setPageAction(newPage)}
      totalPages={totalPages}
    />
  );
};

export default Page;

This integration demonstrates how a cohesive approach combining data fetching, user filtering, and custom pagination can improve both user experience and system performance. Each component is decoupled, making future updates or feature rollouts significantly less risky.

Use Case: Bringing It All Together

In practice, the pagination component is conditionally rendered only when there are multiple pages. This decision logic is crucial in production environments, ensuring that unnecessary UI elements do not load when not needed.

{/* Conditionally render Pagination if more than one page exists */}
{totalPages > 1 && (
   setPageAction(newPage)}
  />
)}

Production Considerations

  • Scalability: Custom pagination allows you to optimize queries, reduce payload size, and improve load times, making it an essential part of scalable architectures.
  • User Experience: An intuitive pagination UI helps users navigate large datasets seamlessly, reducing friction and potentially increasing user engagement.
  • Maintainability: Encapsulating logic within custom hooks and dedicated components not only simplifies the codebase but also accelerates debugging and future enhancements.
  • Performance: By leveraging TanStack Query's caching and background fetching, you can ensure that even data-intensive pages remain performant, which is a direct competitive advantage in today's market.

Conclusion

Custom pagination with TanStack Query in a React and TypeScript application is more than just a UI enhancement it's a strategic production lever. For enterprise-level products, this approach enables efficient data management, responsive user interfaces, and a scalable architecture that aligns with business growth objectives. By following the patterns and techniques outlined in this newsletter, decision-makers can be confident that their systems are both robust and agile, ready to meet the demands of modern users.