> ## Content Index
> Fetch the complete content index at: https://www.angularspace.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Create Pagination Using Firebase and NgRx SignalStore
- URL: https://www.angularspace.com/create-pagination-using-firebase-and-ngrx-signalstore/
- Published: 2024-09-02T07:31:31.000Z
- Updated: 2024-09-02T07:31:31.000Z
- Author: Gabriel Nuñez
- Tags: Angular 17, Signals, Ngrx, firebase, Articles

Some time ago, I had the opportunity to start a simple application. Since I was the only developer and it was a charity project, I had the freedom to choose the tools. One of my main goals was to make the most of the newly released signals without losing the power of RxJS. This article is the result of a small part of that project and some of the implementations I made during the process of creating it.

But why write an article about pagination, you might be thinking. Because I believe it's a great example of how you can combine signals and observables to get the most out of both. Besides, I couldn't find another tutorial using these technologies on the internet. 😅

### The Tools 🛠

#### Firebase 🔥

I chose Firebase because it provides everything I need: authentication, storage, and a real-time database. It simplifies my life significantly since I don't have to worry much about the backend.

#### NgRx SignalStore🚦

It took me a long time to make a decision, as I generally try to avoid using external libraries whenever possible. However, after reading several articles, comparing other solutions, and conducting some proof-of-concept tests, I chose SignalsStore because it met all my needs. Another valid alternative was [signalsSlice](https://ngxtension.netlify.app/utilities/signals/signal-slice/?ref=angularspace.com), but although it is simpler, it is less powerful than SignalsStore.

### Before the start 🚀

It's not entirely necessary, but if you want to follow the article in more detail, you can clone this [repository](https://github.com/gabrieluy/pagination-firebase-signals-store/?ref=angularspace.com) with the example and check it throughout the article. It's preconfigured with Firebase emulators with preloaded data, and instructions on the README, so you can't complain that I didn't make it easy for you. 😊

If everything goes well, you should see the following:

![App dashboard](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/08/app-dashboard-2.png)

localhost:4200

![firestore dashboard](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/08/emulators-dashboard-2.png)

localhost:4000/firestore

### The Implementation 🧑‍💻

The idea here is not to create a step-by-step guide, as that would be long and tedious. Instead, I will showcase the key components of the solution and explain the points or decisions that I found to add the most value. If you want to go deeper, you can always check the repository.

#### The Post and PostsListConfig interfaces 📝

Lets take a look at the `Post` and `PostsListConfig` interfaces:

```typescript
export interface Post {
  id: string;
  title: string;
  description: string;
  date: number;
}

export interface PostsListConfig {
  limit: number;
  page: number;
  pageLastElements: Map<number, Post>;
}

```

The `Post` interface is a simple object with a few properties, nothing fancy.  
The `PostsListConfig` interface is more interesting and is used to configure pagination for the posts. Here's a breakdown of its properties:

- `limit`: The number of posts to display per page.
- `page`: The current page number.
- `pageLastElements`: A map storing the last post of each page, which helps manage pagination efficiently. We will go through this later.

#### The `PostsService` Class 🔧

Now lets take a look at the `PostsService` class.

```typescript
  @Injectable({
    providedIn: 'root',
  })
  export class PostsService {
    private readonly PATH = 'posts';
    private readonly firestore = inject(Firestore);
    private readonly collection = collection(this.firestore, this.PATH).withConverter(assignTypes<Post>());

    getPosts$(config: PostsListConfig): Observable<Post[]> {
      const { limit: qLimit, page, pageLastElements } = config;
      const conditions: QueryConstraint[] = [orderBy('date', 'desc'), limit(qLimit)];

      let postCollection;
      if (page === 1) {
       const { date } = pageLastElements.get(page - 1)!;
      conditions.push(startAfter(date));
        postCollection = query(this.collection, orderBy('date', 'desc'), limit(qLimit));
      } else {
        const { date } = pageLastElements.get(page - 1)!;
        postCollection = query(this.collection, orderBy('date', 'desc'), limit(qLimit), startAfter(date));
      }
      return collectionData(postCollection, { idField: 'id' });
    }
  }

```

The `PostsService` handles data retrieval from Firestore, it worth to mention that im using AngularFire to simplify the process of interacting with Firestore.  
We have 2 scenarios here:

**Scenario 1: If the page is the first**

```typescript
  query(this.collection, orderBy('date', 'desc'), limit(qLimit))

```

Being the first page, no pagination is required, so the query simply orders the posts by date in descending order and limits the results to the specified number (qLimit). This is the simplest scenario.

**Scenario 2: If the page is not the first**

```typescript
  query(this.collection, orderBy('date', 'desc'), limit(qLimit), startAfter(date))

```

Here, things get more interesting. Firebase doesn't support numerical offsets, so pagination is achieved using [query cursors](https://firebase.google.com/docs/firestore/query-data/query-cursors?ref=angularspace.com). This involves the `startAt` and `startAfter` methods, which require a reference to a document in the database. The difference between the two is:

- `startAt`: Returns documents **greater than or equal** to the reference document.
- `startAfter`: Returns documents **greater than** the reference document.

Similarly, `endAt` and `endBefore` methods can define the end point for the query results.

So for be able to navigate forward and backward through the pages, we need to save the last document of each page in the `pageLastElements` map. So now using this reference we can go forward and backward in the collection. An important thing to take into account here is that in in this example the `orderBy` is hardcoded, but in case of dynamic sorting, we should use the order parameter in the `startAfter` methods.

#### The `PostsStore` 📦

Let's delve into one of the most interesting parts of this article: The `PostsStore`. The PostsStore holds the state of our application. As mentioned earlier, we will use the `SignalStore` from the `ngrx/store` library to create the store. But first, let's explain the store's model.

```typescript
  export type StatusType = 'loaded' | 'loading' | 'success' | 'error';

  export interface Posts {
    entities: Post[];
    entitiesCount: number;
  }

  export interface PostListState {
    listConfig: PostsListConfig;
    posts: Posts
    status: StatusType;
  }

  export const postListInitialState: PostListState = {
    listConfig: {
      page: 1,
      limit: 4,
      pageLastElements: new Map<number, Post>(),
    },
    posts: {
      entities: [],
      entitiesCount: 0,
    },
    status: 'loading',
  };

```

As you can se from the code above, we have a `PostListState` interface that defines the state of our store. It holds the `listConfig` (which we are already familiar with), the `posts` and the `status`. The `posts` attribute holds the posts and the count of posts, while the status attribute indicates the current status of the store. This status is useful for displaying loaders or error/success messages.

The initial state, postListInitialState, simply sets up a starting configuration for the post list.

Now that we have our state, we can check the `PostsStore`:

```typescript
  export const PostsListStore = signalStore(
    { providedIn: 'root' },
    withState<PostListState>(postListInitialState),
    withComputed(store => ({
      paginator: computed(() => ({
        show: store.listConfig.page() > 1 || store.posts.entitiesCount() === store.listConfig.limit(),
        hasPreviousPage: store.listConfig.page() > 1,
        hasNextPage: store.posts.entitiesCount() === store.listConfig.limit(),
      })),
    })),
    withMethods((store, service = inject(PostsService)) => ({
      loadPosts: rxMethod<PostsListConfig>(
        pipe(
          tap(() => patchState(store, { status: 'loading' })),
          switchMap(listConfig =>
            service.getPosts$(listConfig).pipe(
              tapResponse({
                next: (entities: Post[]) => {
                  const newListConfig = listConfig.pageLastElements.set(listConfig.page, entities[entities.length - 1]);
                  patchState(store, {
                    posts: { entitiesCount: entities.length, entities },
                    listConfig: {
                      ...listConfig,
                      pageLastElements: newListConfig,
                    },
                    status: 'success',
                  });
                },
                error: () => {
                  patchState(store, { ...postListInitialState }, { status: 'error' });
                },
              })
            )
          )
        )
      ),
    }))
  );

```

This may look complicated, but it's actually quite simple if we break it down.

- The `withState` feature adds state properties to the `SignalStore`, accepting the initial state as an input argument.
- The `withComputed` feature is used to add computed properties to the `SignalStore`.
- The `withMethods` feature is used to add methods to the `SignalStore`.

In our case since the `withState` feature is simple enough, we will not go into detail about it. So let's start with the `withComputed` feature:

We use `withComputed` to determine the visibility of the paginator based on the current page and the number of entities in the store. This ensures that the paginator buttons are displayed only when needed, which we will see in action later.

For the `withMethods` feature, we use [rxMethod](https://ngrx.io/api/store/rx-method?ref=angularspace.com) to create a method called `loadPosts`. This method receives a `PostsListConfig` object and returns an `Observable` that loads the posts based on the configuration.

If we look closely at the `tapResponse`, we can see how we update the pageLastElements map and then add it to the state using `patchState`. Additionally, we manage the status by setting it to `loading` initially, and then to `success` or `error` based on the outcome of the data fetch.

#### The `PostsComponent` 💻

Now we can start with the most exciting part of the project: using our store to display the posts. One of the best things about `SignalStore` is that it is incredibly easy to use—we only need to inject it and utilize its features. We can consume the state signals like `posts` or call methods like `loadPosts`. Additionally, we can use the computed function to create a computed property that updates when the store changes. In our case, we have `$isLoading`.

```typescript
  export class PostsListComponent {
    readonly listStore = inject(PostsListStore);

    $posts = this.listStore.posts.entities;
    $isLoading = computed(() => this.listStore.status() === 'loading');
    $listConfig = this.listStore.listConfig;

    constructor() {
      this.listStore.loadPosts(this.$listConfig());
    }

    goToNextPage() {
      this.listStore.loadPosts({ ...this.$listConfig(), page: this.$listConfig.page() + 1 });
      window.scroll({ top: 0, left: 0, behavior: 'smooth' });
    }

    goToPrevPage() {
      this.listStore.loadPosts({ ...this.$listConfig(), page: this.$listConfig.page() - 1 });
      window.scroll({ top: 0, left: 0, behavior: 'smooth' });
    }
  }

```

```html
  <div class="w-full pt-6">
    <div class="flex justify-content-center grid grid-cols-4 gap-4 mb-4">
      @if (!$isLoading()) {
        @for (post of $posts().values(); track post.id) {
          <post-card
            class="w-full max-w-20rem"
            [$post]="post" />
        }
        <paginator
          class="w-full"
          (onNextClicked)="goToNextPage()"
          (onPrevClicked)="goToPrevPage()"
          [$paginator]="listStore.paginator()" />
      } @else {
        <post-card-skeleton [$quantity]="listStore.listConfig.limit()" />
      }
    </div>
  </div>

```

#### The `PaginatorComponent` 💻

To wrap up this article, as promised, let's examine the `PaginatorComponent`. This component uses signal inputs and outputs to manage the visibility of the paginator and the buttons for navigating to the previous and next pages. As you can see, the component is quite straightforward and easy to understand.

```html
  @if ($paginator().show) {
    <div class="flex justify-content-between">
      <p-button
        icon="pi pi-arrow-left"
        label="Previous"
        [disabled]="!$paginator().hasPreviousPage"
        [rounded]="true"
        (onClick)="onPrevClicked.emit()"
        [raised]="true"></p-button>

      <p-button
        icon="pi pi-arrow-right"
        label="Next"
        [disabled]="!$paginator().hasNextPage"
        [rounded]="true"
        [raised]="true"
        (onClick)="onNextClicked.emit()"></p-button>
    </div>
  }

```

```typescript
  export class PaginatorComponent {
    $paginator = input.required<PaginatorConfig>();
    onNextClicked = output<void>();
    onPrevClicked = output<void>();
  }

```

#### The Demo 🎨

If you haven't downloaded the repository, you can view the demo below.  
Notice that because we are using a rxMethod, the loaded posts remain connected with the Firebase database, allowing you to see changes in real time.

In case you haven't downloaded the repository, you can view the demo below:  
Notice that because we are using a `rxMethod` the loaded posts remain connected with the firebase database, so you can see the changes in real time.

0:00 

/0:15 

1× 

Final demo

Thank you for following along! I hope this guide helps you create pagination using Firebase in the future and, more importantly, improves your skills with Signals.

And remember, you can check the [repository](https://github.com/gabrieluy/pagination-firebase-signals-store/?ref=angularspace.com) to view the code and run the demo yourself.

---

![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/09/Screenshot-2024-08-05-at-10.51.46.jpg)