# Streamlining Pagination in TypeScript: An Efficient Paginator Class

# 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:

<div data-node-type="callout">
<div data-node-type="callout-emoji">👉</div>
<div data-node-type="callout-text"><a target="_self" rel="noopener noreferrer nofollow" href="https://github.com/OxyProgrammer/efficient-paginator" style="pointer-events: none">https://github.com/OxyProgrammer/efficient-paginator</a></div>
</div>

# Peeking into the Code

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

```typescript
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.
