Handling pagination with NgRx component stores
Tags: angular, ngrx, tutorial, javascript
Published: 2023-02-28 09:38:51

[NgRx](https://ngrx.io/) is a popular state management library for Angular and is widely used to implement the [Redux pattern](https://en.wikipedia.org/wiki/Redux_(JavaScript_library)) in your applications.

However, depending on the use cases, you might not always want to use the whole artillery that a state is made of.

For such scenarios, NgRx introduced a lighter, local way of managing the store by component: the [component stores](https://ngrx.io/guide/component-store).

In this article we will see a quick example of how to setup a simple component store taking advantage of the latest NgRx API and how to use it to paginate a list of items.

> This guide is designed as a step by step lab during which we will gradually implement the pagination with component stores. If you would like to jump straight to the final implementation, click [here](#taking-advantage-of-generics-for-broader-usage)

---

## Table of content

- [Initial Setup](#initial-setup)
  - [Creating the Angular application](#creating-the-angular-application)
  - [Faking our backend](#faking-our-backend)
  - [Creating our component](#creating-our-component)
- [Handling the pagination](#handling-the-pagination)
  - [The simplest way](#the-simplest-way)
  - [With subjects](#with-subjects)
  - [Adding state management](#adding-state-management)

---

# Initial Setup

Our application will simply display a list of paginated todo items.

We won't implement the logic to toggle, edit, add or perform any other action on them since this is not the main focus of this article.

## Creating the Angular application

> For our Angular app, I will use Angular v15.2 which is the latest at the time I am writing this article.

In a new directory, type in the following command:

```sh
ng new --directory ./  --minimal --inline-template --skip-tests
```

You can then leave all the other options to their default value and head of to your favorite code editor and get rid of the initial content of the `app.component.ts` and replace it by the following:

```ts
@Component({
  selector: 'app-root',
  standalone: true,
  template: `
    <h1>Simple pagination with NgRx component stores</h1>
  `,
  styles: []
})
export class AppComponent {
}
```

Since we will be using standalone components, you can also remove the `app.module.ts` and replace the application startup in the `main.ts` with this:

```ts
// main.ts
import { provideHttpClient } from "@angular/common/http";
import { bootstrapApplication } from "@angular/platform-browser";

import { AppComponent } from "./app/app.component";

bootstrapApplication(AppComponent, {
  providers: [provideHttpClient()],
}).catch((err) => console.error(err));

```

We can now hit `ng serve` and proceed with our demo!

## Faking our backend

To retrieve todo items, I will use the excellent [JSON placeholder API](https://jsonplaceholder.typicode.com) to mimick a backend.

For that, let's first create a contract in a newly created file `todo-item.ts`:

```ts
// todo-item.ts
export interface TodoItem {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
}
```

We can now proceed an implement our `TodoItemService` to retrieve them:

```ts
// todo-item.service.ts
import { HttpClient, HttpParams } from "@angular/common/http";
import { inject, Injectable } from "@angular/core";

import { Observable } from "rxjs";

import { TodoItem } from "./todo-item";

@Injectable({ providedIn: "root" })
export class TodoItemService {
  private readonly _http = inject(HttpClient);

  getTodoItems(offset?: number, pageSize?: number): Observable<TodoItem[]> {
    const params = new HttpParams({
      fromObject: {
        _start: offset ?? 0,
        _limit: pageSize ?? 10,
      },
    });

    return this._http.get<TodoItem[]>(
      "https://jsonplaceholder.typicode.com/todos",
      { params }
    );
  }
}
```

## Creating our component

With our service created, we can consume the contract from a new `TodoItemComponent`:

```ts
// todo-item.component.ts
import { NgIf } from "@angular/common";
import { Component, Input } from "@angular/core";

import { TodoItem } from "./todo-item";

@Component({
  selector: "app-todo-item",
  standalone: true,
  imports: [NgIf],
  template: `
    <div *ngIf="todoItem">
      <input type="checkbox" [checked]="todoItem.completed" />
      <span>#{{ todoItem.id }} - {{ todoItem.title }}</span>
    </div>`,
})
export class TodoItemComponent {
  @Input() todoItem?: TodoItem;
}
```

> This is arguably not the most good looking todo item display, I give you that

And finally use our bricks to display our first page:

```ts
// app.component.ts
import { AsyncPipe, NgFor } from "@angular/common";
import { Component, inject } from "@angular/core";

import { TodoItemComponent } from "./todo-item.component";
import { TodoItemService } from "./todo-item.service";

@Component({
  selector: "app-root",
  standalone: true,
  imports: [NgFor, AsyncPipe, TodoItemComponent],
  template: `
    <h1>Simple pagination with NgRx component stores</h1>

    <app-todo-item
      *ngFor="let todoItem of todoItems$ | async"
      [todoItem]="todoItem"
    />
  `,
})
export class AppComponent {
  private readonly _todoItemService = inject(TodoItemService);
  readonly todoItems$ = this._todoItemService.getTodoItems();
}
```

You should new see our homepage with some todo items:

![homepage v1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w3fjim3cvxf1mddri0p1.png)

# Handling the pagination

With our app up and running with some todo items displayed, we can now focus on our pagination issue.

We will just slightly update our `AppComponent` to give our user access to the previous and next page:

```ts
// app.component.ts
// ...

@Component({
  // ...
  template: `
    <h1>Simple pagination with NgRx component stores</h1>

    <app-todo-item
      *ngFor="let todoItem of todoItems$ | async"
      [todoItem]="todoItem"
    />

    <!-- 👇 Pagination buttons -->
    <button type="button" aria-label="Previous Page" (click)="onPreviousPage()">
      ←
    </button>

    <button type="button" aria-label="Next Page" (click)="onNextPage()">
      →
    </button>

  `,
})
export class AppComponent {
  private readonly _todoItemService = inject(TodoItemService);
  readonly todoItems$ = this._todoItemService.getTodoItems();

  // 👇 Associated handlers
  onPreviousPage(): void {}
  onNextPage(): void {}
}
```

## The simplest way

The most simple way could be to introduce a variable holding the pagination details and, on click, update its value an reassign the `todoItems$` observable:

```ts
// app.component.ts
// ...

@Component({
  // ...
})
export class AppComponent {
  private readonly _todoItemService = inject(TodoItemService);
  todoItems$ = this._todoItemService.getTodoItems();

  private _pagination = {
    offset: 0,
    pageSize: 10,
  };

  onPreviousPage(): void {
    this._pagination.offset -= this._pagination.pageSize;

    const { offset, pageSize } = this._pagination;
    this.todoItems$ = this._todoItemService.getTodoItems(offset, pageSize);
  }

  onNextPage(): void {
    this._pagination.offset += this._pagination.pageSize;

    const { offset, pageSize } = this._pagination;
    this.todoItems$ = this._todoItemService.getTodoItems(offset, pageSize);
  }
}
```

However this approach is not very reactive and we must implement the logic imperatively all over the place (which is just two methods in our case).

Surely we can do better.

## With subjects

Using [RxJS `BehaviorSubject`](https://rxjs.dev/api/index/class/BehaviorSubject) we can hold a single state of our pagination details an dynamically react to any change of value to update our queried todo items:

```ts
// app.component.ts
// ...

@Component({
  // ...
})
export class AppComponent {
  private readonly _todoItemService = inject(TodoItemService);

  // 👇 Introducing a subject holding the pagination details
  private readonly _pagination$ = new BehaviorSubject({
    offset: 0,
    pageSize: 10,
  });

  // 👇 Reactively update the todo items on pagination change
  readonly todoItems$ = this._pagination$.pipe(
    switchMap(({ offset, pageSize }) =>
      this._todoItemService.getTodoItems(offset, pageSize)
    )
  );

  onPreviousPage(): void {
    const { offset, pageSize } = this._pagination$.getValue();

    this._pagination$.next({
      offset: offset - pageSize,
      pageSize,
    })
  }

  onNextPage(): void {
    const { offset, pageSize } = this._pagination$.getValue();

    this._pagination$.next({
      offset: offset + pageSize,
      pageSize,
    })
  }
}
```

This may be a better but we still have our pagination logic within our component, along with the state of it.

This looks like a great use case for a component store!

## Adding state management

Before using the component store, we first should install the associated package:

```sh
npm install @ngrx/component-store --save
```

Once the installation is complete, create a new file named `app.component-store.ts` inside which we will first define our state.

Our state should contain:

- The `TodoItem`s to display
- The pagination details (the offset and the page size)

```ts
export interface AppState {
  todoItems: TodoItem[];
  offset: number;
  pageSize: number;
}
```

For our store to be used, we also need an initial state used to initialize the store:

```ts
const initialState: AppState = {
  todoItems: [],
  offset: 0,
  pageSize: 10,
};
```

We can now create our store and initialize it with our initial state:

```ts
@Injectable()
export class AppComponentStore extends ComponentStore<AppState> {
  constructor() {
    super(initialState);
  }
}
```

Finally, if we want to use it, we should provide it to our `AppComponent` using the [`provideComponentStore`](https://ngrx.io/api/component-store/provideComponentStore) method:

```ts
// app.component.ts
@Component({
  // ...
  providers: [
    // 👇 Provide our component store to our component
    provideComponentStore(AppComponentStore),
  ]
})
export class AppComponent {
  // ...
}
```

A common way of using component stores is by exposing a view model as `vm$` for the component to consume.

Let's take advantage of this syntax to remove the former logic from our template:

```ts
// app.component-store.ts
@Injectable()
export class AppComponentStore extends ComponentStore<AppState> {
  readonly vm$ = this.select(({ todoItems }) => ({ todoItems }));

  constructor() {
    super(initialState);
  }
}
```

```ts
// app.component.ts
@Component({
  selector: "app-root",
  standalone: true,
  imports: [NgIf, NgFor, AsyncPipe, TodoItemComponent],
  template: `
    <!-- 👇 Wrap the view model in a container with an async pipe to rerender on new values -->
    <ng-container *ngIf="vm$ | async as vm">
      <h1>Simple pagination with NgRx component stores</h1>

      <!-- 👇 Use the todo items exposed by the view model -->
      <app-todo-item
        *ngFor="let todoItem of vm.todoItems"
        [todoItem]="todoItem"
      />

      <button type="button" aria-label="Previous Page" (click)="onPreviousPage()">
        ←
      </button>

      <button type="button" aria-label="Next Page" (click)="onNextPage()">
        →
      </button>
    </ng-container>
  `,
  providers: [provideComponentStore(AppComponentStore)],
})
export class AppComponent {
  // 👇 Consume the component store and its API instead of
  // handling the logic here
  private readonly _componentStore = inject(AppComponentStore);
  readonly vm$ = this._componentStore.vm$;

  onPreviousPage(): void {}

  onNextPage(): void {}
}
```

We're good to go!

With our component store initialized, we can now move our pagination logic into it.

In this kind of store, as for the more "classical" ones, we can divide our code into three main sections:

- Selectors
- Effects
- Reducer

In a component store, selectors are frequently let aside in favor or the view model.

However, we could fire some effects and have them impact the store.

Using this approach, let's raise some events regarding the page offset and update our state accordingly

```ts
@Injectable()
export class AppComponentStore extends ComponentStore<AppState> {
  private readonly _todoItemService = inject(TodoItemService);

  readonly vm$ = this.select(({ todoItems }) => ({ todoItems }));

  constructor() {
    super(initialState);
  }

  // 👇 Effect loading the todo items
  readonly loadNextPage = this.effect((trigger$: Observable<void>) => {
    return trigger$.pipe(
      withLatestFrom(this.select((state) => state)),
      map(([, state]) => state),
      tap(({ offset }) => this.updateOffset(offset + 1)),
      switchMap(({ offset, pageSize }) =>
        this._todoItemService.getTodoItems(offset * pageSize, pageSize).pipe(
          tapResponse(
            (todoItems: TodoItem[]) => this.updateTodoItems(todoItems),
            () => console.error("Something went wrong")
          )
        )
      )
    );
  });

  // 👇 Updaters for our state
  private readonly updateOffset = this.updater(
    (state: AppState, offset: number) => ({
      ...state,
      offset,
    })
  );

  private readonly updateTodoItems = this.updater(
    (state: AppState, todoItems: TodoItem[]) => ({
      ...state,
      todoItems,
    })
  );
}
```

> The effect for the previous page is pretty much the same, try to implement it yourself!

We can now call our effects:

```ts
// app.component.ts
@Component({ 
  // ...
})
export class AppComponent {
  private readonly _componentStore = inject(AppComponentStore);
  readonly vm$ = this._componentStore.vm$;

  onPreviousPage(): void {}

  onNextPage(): void {
    this._componentStore.loadNextPage();
  }
}
```

Unfortunately, when we visit our page once again, nothing is displayed until `onNextPage` is displayed.

Indeed, if we look at the lifecycle of our component store, we are not loading anything before the first change is requested.

To solve it, we will proceed in two steps:

The first one is to split the loading of the page and the loading of a different offset into two dedicated effects:

```ts
// app.component-store.ts
  readonly loadPage = this.effect((trigger$: Observable<void>) => {
    return trigger$.pipe(
      withLatestFrom(this.select((state) => state)),
      map(([, state]) => state),
      switchMap(({ offset, pageSize }) =>
        this._todoItemService.getTodoItems(offset * pageSize, pageSize).pipe(
          tapResponse(
            (todoItems: TodoItem[]) => this.updateTodoItems(todoItems),
            () => console.error("Something went wrong")
          )
        )
      )
    );
  });

  readonly loadNextPage = this.effect((trigger$: Observable<void>) => {
    return trigger$.pipe(
      withLatestFrom(this.select((state) => state.offset)),
      map(([, state]) => state),
      tap((offset) => this.updateOffset(offset + 1)),
      tap(() => this.loadPage())
    );
  });
```

The second is by taking advantage of the NgRx component store lifecycle hooks as described in [this article](https://dev.to/ngrx/announcing-ngrx-v14-action-groups-componentstore-lifecycle-hooks-eslint-package-revamped-ngrx-component-and-more-18ck) by @brandontroberts 

This article tells us that we can perform an action after the state has been initiated or after the store has been initiated.

In our case, loading the first page once the store has been set up sounds like a great option, let's do that!

```ts
@Injectable()
export class AppComponentStore
  extends ComponentStore<AppState>
  // 👇 Implement the hook
  implements OnStoreInit
{
  private readonly _todoItemService = inject(TodoItemService);

  readonly vm$ = this.select(({ todoItems }) => ({ todoItems }));

  constructor() {
    super(initialState);
  }

  // 👇 Load the page once the store is initialized
  ngrxOnStoreInit() {
    this.loadPage();
  }

  // ...
}
```

If we refresh our page, we can see that our fix is working and that we have successfully implemented pagination using a component store.

![Result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/285wmcmr1e9tei6lzvg3.gif)

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

- Create the effect loading the previous page
- Handle offset boundaries (like not going under 0)
- Add a loading and an error state
- Change the page size

> If you would like to check the resulting code, you can head on to the associated [GitHub repository](https://github.com/pBouillon/DEV.SimplePaginationWithNgRxComponentStores/tree/1.0.0)

In a next article, we will see some other usages such as how to take advantage of generics to make this component store reusable across our application, stay tuned!

---

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

{% embed https://dev.to/pbouillon %}

---

Photo by <a href="https://unsplash.com/@roman34nov?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Roman Trifonov</a> on <a href="https://unsplash.com/photos/67rnodKzsRQ?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>