Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
March 20, 2023 06:09 am GMT

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
    • The state
    • The store itself
    • Setting the foundations
    • Creating selectors
    • Handling pagination
    • Using NgRx lifecycle hooks
  • Introducing generics

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;}

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;}

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

export interface PageContent {  todoItems: TodoItem[];}

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

export interface PaginatedItemsState {  paginationDetails: PaginationDetails;  pageContent: PageContent;}

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` hereexport abstract class PaginatedItemsComponentStore  extends ComponentStore<PaginatedItemsState> { }

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  );}

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 })  );}

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 */}

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();  }}

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.tsconst initialState: PaginatedItemsState = {  paginationDetails: {    offset: 0,    pageSize: 10,  },  pageContent: {    todoItems: [],  },};

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);  }}

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[];}

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>;}

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 { /* ... */ }

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  );
  • 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 })  );

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[]>;

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")          )        )      )    );  });

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 }));}

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);  }

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


Original Link: https://dev.to/this-is-angular/reusable-component-store-for-pagination-using-generics-1na6

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To