Handling Pagination with StateAdapt

handling-pagination-with-stateadapt

In a previous article, I covered how to paginate data using NgRx Component Stores

In today’s article, we are going to tackle the same problem but with StateAdapt, 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

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:

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

Once installed, we can use the default store provider:

// 📁 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:

// 📁 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:

// 📁 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:

// 📁 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.

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

// 📁 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 TodoItems drills down to managing an EntityState:

// 📁 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:

// 📁 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:

// 📁 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, 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:

// 📁 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:

// 📁 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 TodoItems 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 TodoItems every time the pagination changes.

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

// 📁 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:

// 📁 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:

// 📁 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:

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:

// 📁 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:

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

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

    
`
, }) 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

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:

GitHub logo

pBouillon
/
DEV.HandlingPaginationWithStateAdapt

Demo code for the “Handling pagination with StateAdapt” article on DEV

Handling pagination with StateAdapt

Demo code for the “Handling pagination with StateAdapt” article on DEV

I hope that you learn something useful there!

Photo by Sincerely Media on Unsplash

Total
0
Shares
Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Post
key-strategies-to-conduct-regression-testing-for-app-modernization

Key Strategies to Conduct Regression Testing for App Modernization

Next Post
reshoring-and-nearshoring-rise-amidst-china-trouble

Reshoring and Nearshoring Rise Amidst China Trouble

Related Posts