Streamlining Pagination in TypeScript: An Efficient Paginator Class

Siddhartha SSiddhartha S
3 min read

Introduction

Pagination is a crucial aspect of frontend engineering, and no frontend developer can claim to have avoided the need for it. There are standard methods for implementing pagination, and they are well understood. In this article, I present a paginator designed to abstract pagination into a separate layer, thereby keeping the viewing component code clean. Additionally, it utilizes caching to make fast navigation through pages.

About the paginator

Here’s how the final page utilizing the paginator looks:

You can find the entire code at:

Peeking into the Code

The paginator is a generic TypeScript class that provides on-demand iteration.

export default class EfficientPaginator<T> {
  private currentPage: number;
  private pageSize: number;
  private hasMore: boolean;
  private cache: Map<number, FetchResponse<T>>;
  private fetchFunction: FetchFunction<T>;

  constructor(pageSize: number, fetchFunction: FetchFunction<T>) {
    this.currentPage = 0;
    this.pageSize = pageSize;
    this.hasMore = true;
    this.cache = new Map();
    this.fetchFunction = fetchFunction; // Assign the fetch function
  }

  async getItems(direction: Direction): Promise<T[]> {
    if (direction === Direction.Next) {
      this.currentPage++;
    } else if (direction === Direction.Previous && this.currentPage > 1) {
      this.currentPage--;
    }


    let cacheEntry = this.cache.get(this.currentPage);
    if (cacheEntry) {
      this.hasMore = cacheEntry.hasMore;
      return cacheEntry.users;
    }

    try {
      const response = await this.fetchFunction(
        this.currentPage,
        this.pageSize
      );
      this.cache.set(this.currentPage, response);
      this.hasMore = response.hasMore;
      return response.users;
    } catch (error) {
      console.error('Error fetching items:', error);
      return [];
    }
  }

  hasPrevious(): boolean {
    return this.currentPage > 1;
  }

  hasNext(): boolean {
    return this.hasMore;
  }

  getCurrentPage(): number {
    return this.currentPage;
  }

  getPageSize(): number {
    return this.pageSize;
  }
}

export interface FetchFunction<T> {
  (page: number, pageSize: number): Promise<FetchResponse<T>>;
}

export interface FetchResponse<T> {
  users: T[];
  hasMore: boolean;
}
  • An instance of EfficientPaginator is initialized with the page size and a fetchFunction.

  • The page size remains constant throughout the lifetime of this instance, as does the fetch function.

  • Caching is implemented using a map that takes the page number as a key and stores the returned items as an array of items returned by the fetch function.

  • The paginator exposes hasPrevious and hasNext as boolean values, enabling the calling function to be immediately aware of the previous and next page's state and availability.

  • The currentPage and pageSize are also exposed to assist the calling function in providing visual aids to the user.

  • The paginator enforces a specific signature for the fetchFunction (FetchFunction<T>) and the schema of the response (FetchResponse<T>) that it receives from the function.

Caveats

There are a few caveats with this technique, but they can be easily managed by making client-side caching optional.

  • The returned objects are cached on the UI side, and if the number becomes too large, the browser may experience slowdowns.

  • Additionally, if client-side caching is enabled, it may display stale data that could have changed on the server. Changing the page size using a new instance of Paginator will start afresh though, ensuring that the latest data is shown.

Improvements

The intention of the code and demo is to illustrate the concept of abstracting pagination logic rather than creating a full-fledged component for reuse. Consequently, the paginator presented in the demo is not perfect. There are opportunities for improvement, and I welcome anyone interested to submit a pull request addressing the following issues:

  • Exposing the current page and total pages through the paginator. This would require the FetchResponse class to be modified to include the total pages returned by the server.

  • Making client-side caching optional to address the concerns mentioned in the caveats.

  • Currently, if there is an issue calling the fetch function, the paginator logs the error and returns an empty array. It could be improved by propagating a valid exception to the caller.

Conclusion

In this article, we explored a TypeScript paginator class that implements client-side caching and provides a low-latency experience for users. It also encapsulates the pagination.

0
Subscribe to my newsletter

Read articles from Siddhartha S directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Siddhartha S
Siddhartha S

With over 18 years of experience in IT, I specialize in designing and building powerful, scalable solutions using a wide range of technologies like JavaScript, .NET, C#, React, Next.js, Golang, AWS, Networking, Databases, DevOps, Kubernetes, and Docker. My career has taken me through various industries, including Manufacturing and Media, but for the last 10 years, I’ve focused on delivering cutting-edge solutions in the Finance sector. As an application architect, I combine cloud expertise with a deep understanding of systems to create solutions that are not only built for today but prepared for tomorrow. My diverse technical background allows me to connect development and infrastructure seamlessly, ensuring businesses can innovate and scale effectively. I’m passionate about creating architectures that are secure, resilient, and efficient—solutions that help businesses turn ideas into reality while staying future-ready.