Handling Pagination with StateAdapt
Tags: angular, tutorial, beginners, typescript
Published: 2023-12-28 12:00:00

In a previous article, I covered how to paginate data using NgRx Component Stores {% embed https://dev.to/this-is-angular/handling-pagination-with-ngrx-component-stores-1j1p %}

In today's article, we are going to tackle the same problem but with [StateAdapt](https://state-adapt.github.io), a small but growing state management library, focused on composability, evolutivity and declarative programming.

> 📑 As usual, this article is build as a hands on lab. You can code alongside me by following the notes marked by a '🧪'.

---

## Initial State

The application we will be working on will be a simple todo list, with a bunch of items displayed:

![Initial State](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ykqtlhw5az4dqxnklrzt.gif)

For now the pagination is handled by the component itself, along with the data, but we will be shifting this from the component into a dedicated store.

## Configuring StateAdapt

> 🧪 Checkout the `initial-setup` tag to get started from here

Before managing our state, we will first need to configure the state management library.

For that, we will install the required packages:

```sh
npm i -D @state-adapt/core @state-adapt/rxjs @state-adapt/angular
```

Once installed, we can use the default store provider:

```ts
// 📁 src/main.ts
bootstrapApplication(AppComponent, {
  providers: [defaultStoreProvider],  // 👈 Provided here
}).catch((err) => console.error(err));
```

We're all set and ready for to create our adapters!

## Creating the adapters

> 🧪 Checkout the `with-state-adapt` tag to get started from here

StateAdapt has the unique particularity of allowing us to define how to manage a specific piece of data through dedicated adapters.

In our case, we will be managing todo items based on the current pagination.

Instead of managing everything at once, we will break it down by managing each piece of part of our state.

### Adapting the Pagination

To get started, we will start by creating the adapter of the pagination details.

In a new file, we can extract the interface:

```ts
// 📁 src/app/pagination.ts
export interface Pagination {
  offset: number;
  pageSize: number;
}
```

From there, we will be able to define the two actions we currently have: going to the next and the previous page:

```ts
// 📁 src/app/pagination.ts

// ...

export const paginationAdapter = createAdapter<Pagination>()({
  nextPage: ({ offset, pageSize }) => ({ pageSize, offset: offset + 1 }),
  previousPage: ({ offset, pageSize }) => ({ pageSize, offset: offset - 1 }),
  selectors: {
    pagination: (state) => state,
  },
});
```

> 📑 You might also want to change the page size and more in a real application, I'm just sticking to the example here!

### Adapting the Todo Item

As we just did for the pagination, we will now be creating an adapter for our `TodoItem`.

This time the interface already exists, and we will just append the adapter.

For the example simplicity's sake, our state won't contain any logic, just a selector for the item itself:

```ts
// 📁 src/app/todo-item.ts

// ...

export const todoItemAdapter = createAdapter<TodoItem>()({
  selectors: {
    todoItem: (state) => state,
  },
});
```

We're all set for adapting our `TodoItem` but unfortunately we won't be manipulating a single todo item but several onces.

We could create an adapter for `TodoItem[]` but instead StateAdapt offers an efficient and concise way of reusing existing logic for multiple entities with [`createEntityAdapter`](https://state-adapt.github.io/adapters/core#createEntityAdapter).

With our previously defined adapter, we can now adapt a list of todo items fairly simply:

```ts
// 📁 src/app/todo-item.ts

// ...

export const todoItemsAdapter = createEntityAdapter<TodoItem>()(todoItemAdapter);
```

Thanks to that, our adapters for the `TodoItem` are done, in just a few lines of code. It's time to work on the paginated items now!

### Adapting the Paginated Items

Back in our `TodoItemService`, we can define our state as a pagination details and a collection of todo items.

However, since the listed `TodoItem[]` are considered as entities, we will tag them as such. StateAdapt provides a type named `EntityState` that has two generic parameters: the entity itself and the name of the key used for its identity. In our case, managing several `TodoItem`s drills down to managing an `EntityState<TodoItem, 'id'>`:

```ts
// 📁 src/app/todo-item.service.ts

// ...

export interface TodoItemsState {
  pagination: Pagination;
  todoItems: EntityState<TodoItem, 'id'>;
}

@Injectable({ providedIn: 'root' })
export class TodoItemService {
  // ...
}
```

While this might look strange at first, it allows us to reuse or previous adapters to adapt the `TodoItemsState` using `joinAdapters`, with little to no code to write:

```ts
// 📁 src/app/todo-item.service.ts

// ...

export const todoItemsStateAdapter = joinAdapters<TodoItemsState>()({
  pagination: paginationAdapter,
  todoItems: todoItemsAdapter,
})();

@Injectable({ providedIn: 'root' })
export class TodoItemService {
  // ...
}
```

## Creating Our Store

> 🧪 Checkout the `with-adapters` tag to get started from here

Now that all managed entities and state can be adapted, we still have to use the adapters to create the store.

First, we need a starting point, an initial state:

```ts
// 📁 src/app/todo-item.service.ts

// ...

const initialState: Readonly<TodoItemsState> = {
  pagination: { offset: 0, pageSize: 5 },
  todoItems: createEntityState<TodoItem, 'id'>(),
};

@Injectable({ providedIn: 'root' })
export class TodoItemService {
  // ...
}
```

> 📑 Since `todoItems` is an `EntityState<TodoItem, 'id'>`, we use the provided `createEntityState` method for the initial value instead of an empty array.

With this initial state, creating our store doesn't require much more code:

```ts
// 📁 src/app/todo-item.service.ts

// ...

@Injectable({ providedIn: 'root' })
export class TodoItemService {
  readonly #store = adapt(initialState, {
    adapter: todoItemsStateAdapter,
  });

  // ...
}
```

## Defining the Actions

With our state initialized and our store set up, we can now wire the actions allowing us to navigate between pages.

In StateAdapt, an action is triggered by nexting a `Source` that acts as a medium of action emission.

In our case, we will need two source: one to navigate to the next page, and one to navigate to the previous one:

```ts
// 📁 src/app/todo-item.service.ts

// ...

@Injectable({ providedIn: 'root' })
export class TodoItemService {
  readonly nextPage$ = new Source('[Todo Items State] Next Page');
  readonly previousPage$ = new Source('[Todo Items State] Previous Page');

  readonly #store = adapt(initialState, {
    adapter: todoItemsStateAdapter,
    sources: {
      previousPaginationPage: this.previousPage$,
      nextPaginationPage: this.nextPage$,
    },
  });

  // ...
}
```

> 💡 Notice that StateAdapt auto-generated the sources based on the joined `paginationAdapter`!

While this will update the pagination, the `TodoItem`s won't be updated, and we will need to perform a side effect for that.

In essence, we would like to retrieve and set the `TodoItem`s every time the pagination changes.

To do so, we will first define a way to set all `TodoItem`s, the same way we did for the pagination:

```ts
// 📁 src/app/todo-item.service.ts

// ...

@Injectable({ providedIn: 'root' })
export class TodoItemService {
  readonly nextPage$ = new Source('[Todo Items State] Next Page');
  readonly previousPage$ = new Source('[Todo Items State] Previous Page');
  readonly #setTodoItems$ = new Source<TodoItem[]>(
    '[Todo Items State] Set Todo Items'
  );

  readonly #store = adapt(initialState, {
    adapter: todoItemsStateAdapter,
    sources: {
      previousPaginationPage: this.previousPage$,
      nextPaginationPage: this.nextPage$,
      setTodoItemsAll: this.#setTodoItems$,  👈 New source
    },
  });

  // ...
}
```

Performing a side effect is just as much as any side effect defined using RxJs: by subscribing to the appropriate observable:

```ts
// 📁 src/app/todo-item.service.ts

// ...

@Injectable({ providedIn: 'root' })
export class TodoItemService {
  readonly nextPage$ = new Source<void>('[Todo Items State] Next Page');
  readonly previousPage$ = new Source<void>('[Todo Items State] Previous Page');

  // 👇 Since this side effect is internal, the visibility is private
  readonly #setTodoItems$ = new Source<TodoItem[]>('[Todo Items State] Set Todo Items');

  readonly #store = adapt(initialState, {
    // ...
  });

  constructor() {
    this.#store.pagination$
      .pipe(
        takeUntilDestroyed(),
        switchMap((pagination) => this.getTodoItems(pagination))
      )
      .subscribe((todoItems) => this.#setTodoItems$.next(todoItems));
  }

  // ...
}
```

We are almost done! Our state is now a functional management solution, but we can't read it for now, let's add the selectors.

## Reading Our State

A popular way of reading state is by defining view models, or "vm".

For our store, we can define the view model as a signal of the two things we are interested in: the pagination and the todo items:

```ts
// 📁 src/app/todo-item.service.ts

// ...

@Injectable({ providedIn: 'root' })
export class TodoItemService {
  // ...

  readonly vm = toSignal(
    this.#store.state$.pipe(
      map((state) => ({
        pagination: state.pagination,
        todoItems: Object.values(state.todoItems.entities),
      }))
    ),
    { requireSync: true }
  );

  constructor() {
    // ...
  }

  // ...
}
```

> 📑 You can also use `combineLatest` to create your view model:
> ```ts
readonly vm = toSignal(
  combineLatest({
    pagination: this.#store.pagination$,
    todoItems: this.#store.todoItemsAll$,
  }),
  { requireSync: true }
);
```

Our state is now initialized and readable, it's time to ditch the logic in the component and rely on it instead!

## Consuming the State

> 🧪 Checkout the `with-store` tag to get started from here

Back in our `AppComponent`, we can now remove the custom logic from the code behind and rely on the service instead:

```ts
// 📁 src/app/app.component.ts

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

  readonly vm = this.#todoItemService.vm;

  onPreviousPage(): void {
    this.#todoItemService.previousPage$.next();
  }

  onNextPage(): void {
    this.#todoItemService.nextPage$.next();
  }
}
```

Similarly, we can now consume the `vm` from the template:

```ts
// 📁 src/app/app.component.ts

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [TodoItemComponent],
  template: `
    @for (todoItem of vm().todoItems; track todoItem.id) {
    <app-todo-item [todoItem]="todoItem" />
    }

    <div class="grid">
      <button
        type="button"
        (click)="onPreviousPage()"
        [disabled]="vm().pagination.offset === 0"
      >
        ←
      </button>
      <button
        type="button"
        (click)="onNextPage()"
        [disabled]="vm().pagination.offset === 2"
      >
        →
      </button>
    </div>
  `,
})
export class AppComponent {
  // ...
}
```

> 🧪 You could still go further, what about moving the condition for the `[disabled]` attributes to a selector?

And we are done! However, our pagination is now handled by StateAdapt that will also give us access to additional features like Redux DevTools out of the box:

![Finalized](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/87idef3kd6f72gtld3y2.gif)

##  Takeaways

In this article, we saw how we could handle pagination using StateAdapt as our state management solution, by taking advantage of its API focused on composition.

We incrementally adapted our entities to create the component state, and initialized our store from that point.

Finally, we consumed it from our component in order to remove the logic that it defined.

> If you would like to see the resulting code, you can browse the article's repository:

> {% embed https://github.com/pBouillon/DEV.HandlingPaginationWithStateAdapt %}

---

I hope that you learn something useful there!

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

---

> Photo by <a href="https://unsplash.com/@sincerelymedia?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Sincerely Media</a> on <a href="https://unsplash.com/photos/open-book-lot-IKzmglo7JLk?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Unsplash</a>