DEV Community

Cover image for Reusable component store for pagination using generics
Pierre Bouillon for This is Angular

Posted on • Updated on

Reusable component store for pagination using generics

In the last article we created a component store using NgRx to handle the pagination of todo items

Unfortunately, this approach is very specific to our domain and it is now time for us to look for a reusable solution that we can slap on another entity that we would like to paginate the same way


Table of content


Creating our new store

Let's begin by creating a new paginated-items.component-store.ts file where our
pagination logic will take place.

The state

Before defining our store, we will first have to focus on its state

Here is our state in its current form:

export interface AppState {
  todoItems: TodoItem[];
  offset: number;
  pageSize: number;
}
Enter fullscreen mode Exit fullscreen mode

We can divide its properties into two main categories:

  • Everything related to the pagination (offset, pageSize)
  • Everything related to the items themselves (only todoItems in our case)

From this, we can extract a subsequent interface, which has the responsibility of wrapping the parameters related to the pagination:

export interface PaginationDetails {
  offset: number;
  pageSize: number;
}
Enter fullscreen mode Exit fullscreen mode

And another one which will wrap the content of the page:

export interface PageContent {
  todoItems: TodoItem[];
}
Enter fullscreen mode Exit fullscreen mode

Using these two interfaces, we now have a state that is a bit more explicit:

export interface PaginatedItemsState {
  paginationDetails: PaginationDetails;
  pageContent: PageContent;
}
Enter fullscreen mode Exit fullscreen mode

Great, let's move on!

The store itself

Setting the foundations

This store will be defined as an abstract class so that each subsequent store can add its own logic to is.

Using the previously defined state, we can write this:

@Injectable()
// πŸ‘‡ Beware not to forget the `abstract` here
export abstract class PaginatedItemsComponentStore
  extends ComponentStore<PaginatedItemsState> { }
Enter fullscreen mode Exit fullscreen mode

Creating selectors

Having state management is great, being able to access its content is better: let's add some selectors.

We can start by adding some base ones, to retrieve each property:

@Injectable()
export abstract class PaginatedItemsComponentStore
  extends ComponentStore<PaginatedItemsState>
{
  readonly selectPaginatedItemsState = this.select((state) => state);

  readonly selectPaginationDetails = this.select(
    this.selectPaginatedItemsState,
    ({ paginationDetails }) => paginationDetails
  );

  readonly selectOffset = this.select(
    this.selectPaginationDetails,
    ({ offset }) => offset
  );

  readonly selectPageSize = this.select(
    this.selectPaginationDetails,
    ({ pageSize }) => pageSize
  );

  readonly selectPageContent = this.select(
    this.selectPaginatedItemsState,
    ({ pageContent }) => pageContent
  );

  readonly selectTodoItems = this.select(
    this.selectPageContent,
    ({ todoItems }) => todoItems
  );
}
Enter fullscreen mode Exit fullscreen mode

That's a lot of selectors but keep in mind that the purpose of this store is to be flexible enough so that any store that inherits from it can use those internals instead of rewriting them somewhere else

Handling pagination

From our store, let's add some updaters to update our state:

@Injectable()
export abstract class PaginatedItemsComponentStore
  extends ComponentStore<PaginatedItemsState>
{
  /* Selectors omitted here */

  private readonly updatePagination = this.updater(
    (state, paginationDetails: PaginationDetails) => ({ ...state, paginationDetails })
  );

  private readonly updatePaginatedItems = this.updater(
    (state, pageContent: PageContent) => ({ ...state, pageContent })
  );
}
Enter fullscreen mode Exit fullscreen mode

I will be using one for the pagination and one for the paginated items but feel free to be more or less granular if you want to!

Finally, we can copy, paste and adapt a bit our two previous loadPage and loadNextPage effects from our AppComponentStore:

@Injectable()
export abstract class PaginatedItemsComponentStore
  extends ComponentStore<PaginatedItemsState>
{
  /* Selectors omitted here */

  // πŸ‘‡ Don't forget to also inject our service
  private readonly _todoItemService = inject(TodoItemService);

  readonly loadPage = this.effect((trigger$: Observable<void>) => {
    return trigger$.pipe(
      // πŸ‘‡ We can directly access our pagination details from our selector
      withLatestFrom(this.selectPaginationDetails),
      switchMap(([, { offset, pageSize }]) =>
        this._todoItemService.getTodoItems(offset, pageSize).pipe(
          tapResponse(
            (todoItems: TodoItem[]) => this.updatePaginatedItems({ todoItems }),
            () => console.error("Something went wrong")
          )
        )
      )
    );
  });

  readonly loadNextPage = this.effect((trigger$: Observable<void>) => {
    return trigger$.pipe(
      // πŸ‘‡ Same here
      withLatestFrom(this.selectPaginationDetails),
      tap(([, { offset, pageSize }]) => this.updatePagination({
        offset: offset + pageSize,
        pageSize
      })),
      tap(() => this.loadPage())
    );
  });

  /* Updaters omitted here */
}
Enter fullscreen mode Exit fullscreen mode

Using NgRx lifecycle hooks

As we did in our first version, we can also take advantage of the OnStoreInit lifecycle hook here to load the page on startup:

@Injectable()
export abstract class PaginatedItemsComponentStore
  extends ComponentStore<PaginatedItemsState>
  implements OnStoreInit
{
  ngrxOnStoreInit() {
    this.loadPage();
  }
}
Enter fullscreen mode Exit fullscreen mode

By doing so, we can ensure that each store paginating items, and thus inheriting this one, will load the first page upon creation

Inheriting from our store

Finally, all that is left to do is for our AppComponentStore to inherit from our PaginatedItemsComponentStore instead of ComponentStore<AppState>

For that, we need to change our initial state and get rid of our own AppState to use the provided PaginatedItemsState:

// app.component-store.ts
const initialState: PaginatedItemsState = {
  paginationDetails: {
    offset: 0,
    pageSize: 10,
  },
  pageContent: {
    todoItems: [],
  },
};
Enter fullscreen mode Exit fullscreen mode

Once this is done, we can delete all updaters, effects and lifecycle hooks implementations from our component store and use instead the logic of the PaginatedComponentStore:

@Injectable()
export class AppComponentStore
  extends PaginatedItemsComponentStore
{
  readonly vm$ = this.select(
    this.selectTodoItems,
    (todoItems) => ({ todoItems }));

  constructor() {
    super(initialState);
  }
}
Enter fullscreen mode Exit fullscreen mode

If you run your application after this change, everything should still be working as it was before, yay!

Introducing generics

So far so good but let's not rejoice so fast

If everything as been as easy as copying and pasting code from our former store to the new one, it is only because we are constraining it to TodoItems

In a regular application, you may (surely) have more than one type of entity to paginate

Fortunately, TypeScript handles generics pretty well and this is something we can leverage to increase the reusability of our PaginatedItemsComponentStore

Rework our state

The first place to look at is our PageContent interface

In this one, we are defining the content as an array of TodoItem but we want to allow for a broader set of types

We may be tempted to change it to any[] but since we are using _Type_Script and not _Any_Script, we may find a better way

Using generics, we can specify to our interface that we would like to have an array of items whose type will be TItem since we don't know it yet:

export interface PageContent<TItem> {
  // πŸ‘‡ Since we are manipulating items now I renamed the property
  items: TItem[];
}
Enter fullscreen mode Exit fullscreen mode

By convention, I'm naming any generic type by starting with a T followed by its logical meaning

Updating this interface will need us to rewrite the PaginatedItemsState as well since we need to propagate the generics:

export interface PaginatedItemsState<TItem> {
  paginationDetails: PaginationDetails;
  pageContent: PageContent<TItem>;
}
Enter fullscreen mode Exit fullscreen mode

Updating our store

With the updates made to the state, our store is no longer valid and we also need to propagate the generic type here

However, we don't want to use a concrete type yet or all our modifications would have been done for nothing

To address the fist compilation error, we will first need to also indicate our store that we will be using TItem:

@Injectable()
export abstract class PaginatedItemsComponentStore<TItem>
  extends ComponentStore<PaginatedItemsState<TItem>>
  implements OnStoreInit { /* ... */ }
Enter fullscreen mode Exit fullscreen mode

After doing so, there is two small errors we need to address:

  • In our selectTodoItems selector, todoItems does no longer exists since we have rename it to items. We can fix it by changing the property name:
  readonly selectItems = this.select(
    this.selectPageContent,
    ({ items }) => items
  );
Enter fullscreen mode Exit fullscreen mode
  • In our updatePaginatedItems updated, PageContent is not aware of the generic type and we need to specify it:
  private readonly updatePaginatedItems = this.updater(
    (state, pageContent: PageContent<TItem>) => ({ ...state, pageContent })
  );
Enter fullscreen mode Exit fullscreen mode

However, we now face a bigger issue: in the loadPage effect, we are calling the todoItemService and this service is very specific to our TodoItems

Delegate the fetching logic

From our PaginatedItemsComponentStore, there is no way for us to know in advance how a specific kind of TItem will be retrieved given an offset and the page size

However, a class that will know that is the implementing one

Fortunately, we are in an abstract class and we can let the child class define its own logic by adding an abstract method:

protected abstract getItems(paginationDetails: PaginationDetails): Observable<TItem[]>;
Enter fullscreen mode Exit fullscreen mode

Using this method, we can now remove the instance of our service and replace its call by the abstract method:

-  private readonly _todoItemService = inject(TodoItemService);

  readonly loadPage = this.effect((trigger$: Observable<void>) => {
    return trigger$.pipe(
      withLatestFrom(this.selectPaginationDetails),
      switchMap(([, { offset, pageSize }]) =>
-       this._todoItemService.getTodoItems(offset, pageSize).pipe(
+       this.getItems({ offset, pageSize }).pipe(
          tapResponse(
-           (todoItems: TodoItem[]) => this.updatePaginatedItems({ todoItems }),
+           (items: TItem[]) => this.updatePaginatedItems({ items }),
            () => console.error("Something went wrong")
          )
        )
      )
    );
  });
Enter fullscreen mode Exit fullscreen mode

Updating the AppComponentStore

We're almost done! Now that our base store is generic, we need to specify in our AppComponentStore that TItem will be TodoItem for us

// πŸ‘‡ Notice that we are now talking about `TodoItem`
const initialState: PaginatedItemsState<TodoItem> = {
  paginationDetails: {
    offset: 0,
    pageSize: 10,
  },
  pageContent: {
    items: [],
  },
};

@Injectable()
export class AppComponentStore
  // πŸ‘‡ Same here
  extends PaginatedItemsComponentStore<TodoItem>
{
  readonly vm$ = this.select(
    // πŸ‘‡ Don't forget that our selector has been renamed
    this.selectItems,
    (todoItems) => ({ todoItems }));
}
Enter fullscreen mode Exit fullscreen mode

However, we now also need to implement that getItems method so that our parent component knows how to retrieve those TodoItems

For that, we will need to reinject a TodoItemService instance and call it from there:

  private readonly _todoItemService = inject(TodoItemService);

  protected getItems({ offset, pageSize }: PaginationDetails): Observable<TodoItem[]> {
    return this._todoItemService.getTodoItems(offset, pageSize);
  }
Enter fullscreen mode Exit fullscreen mode

Building our app again, everything should still be working as before but, this time, paginating a new type of entity won't need you to rewrite the whole component store again!


In this article we saw how to take advantage of generics to lift the common pagination logic to an abstract component that we can later extend

If you would like to go a bit further you can try to:

  • Handle the loading and error logic
  • Add extra selectors (first item of the page, etc.)
  • Create a new PostService that is almost the same as the TodoItemService except that it is retrieving posts by calling https://jsonplaceholder.typicode.com/posts. You can then use this service to paginate Posts instead of TodoItems by defining a new component store inheriting from PaginatedItemsComponentStore<Post>

If you would like to check the resulting code, you can head on to the associated GitHub repository


I hope that you learnt something useful there and, as always, happy coding!


Photo by Roman Trifonov on Unsplash

Top comments (2)

Collapse
 
nazariussss profile image
Nazarius

Very interesting post

Collapse
 
pbouillon profile image
Pierre Bouillon

Thanks a lot, glad you enjoyed it!