Improve data service connectivity in Signal Stores using the withDataService Custom Store Feature
Tags: angular, typescript, signals, javascript
Published: 2024-02-07 09:23:03

---
title: Improve data service connectivity in Signal Stores using the withDataService Custom Store Feature
published: true
tags: #angular #typescript #signals #javascript
canonical_url: https://www.angularaddicts.com/p/signal-store-with-data-service
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nixnexsa1jsc4zbycgg5.png
# Use a ratio of 100:42 for best results.
# published_at: 2024-02-07 09:19 +0000
---


**In this article, I'm going to show you why NgRx SignalStore's Custom Store Features is a game changer: in my example, you'll see how I used it to connect a store to a data service using my `withDataService` Custom Store Feature. Naturally, there are countless other use cases, please let me know in the comments if you created experiments or demo projects.**

**Here are some prerequisites, you should understand how Signals and SignalStore work before you dive in:**

**Angular Signals** is a new reactivity model introduced in Angular 16. The Signals feature helps us track state changes in our applications and triggers optimized template rendering updates. If you are new to Signals, here are some highly recommended articles as a starting point:



* [Official Angular Signals documentation](https://angular.io/guide/signals)
* [“Signals in Angular – How to Write More Reactive Code”](https://www.freecodecamp.org/news/angular-signals/) by [Deborah Kurata](https://twitter.com/DeborahKurata)
* [“Angular & signals. Everything you need to know”](https://dev.to/this-is-angular/angular-signals-everything-you-need-to-know-2b7g) by [Robin Goetz](https://twitter.com/goetzrobin)

The NgRx team and [Marko Stanimirović](https://twitter.com/MarkoStDev) created a signal-based state management solution, [SignalStore](https://ngrx.io/guide/signals/signal-store). If you are new to Signal Stores, you should read [Manfred Steyer](https://twitter.com/ManfredSteyer)'s four-part series about the NgRx Signal Store:



* [The new NGRX Signal Store for Angular: 3+n Flavors](https://www.angulararchitects.io/en/blog/the-new-ngrx-signal-store-for-angular-2-1-flavors/)
* [Smarter, Not Harder: Simplifying your Application With NGRX Signal Store and Custom Features](https://www.angulararchitects.io/en/blog/smarter-not-harder-simplifying-your-application-with-ngrx-signal-store-and-custom-features/)
* [NGRX Signal Store Deep Dive: Flexible and Type-Safe Custom Extensions](https://www.angulararchitects.io/en/blog/ngrx-signal-store-deep-dive-flexible-and-type-safe-custom-extensions/)
* [The NGRX Signal Store and Your Architecture](https://www.angulararchitects.io/en/blog/the-ngrx-signal-store-and-your-architecture/)

**I believe that the most powerful part of the NgRx SignalStore is the [Custom Store Features](https://ngrx.io/guide/signals/signal-store/custom-store-features). It provides a mechanism to extend the functionality of SignalStores in a reusable way.**

To experiment with **Custom Store Features**, I created one named `withDataService`. It connects a data service to the store and tracks the state of the HTTP request. The feature was inspired by:



* the custom features described in [Manfred Steyer](https://twitter.com/ManfredSteyer)'s [NGRX Signal Store Deep Dive: Flexible and Type-Safe Custom Extensions](https://www.angulararchitects.io/en/blog/ngrx-signal-store-deep-dive-flexible-and-type-safe-custom-extensions/) article and the referenced [Source Code](https://github.com/manfredsteyer/standalone-example-cli/tree/arc-signal-store-custom-examples-typed)
* the custom features in [Marko Stanimirović](https://twitter.com/MarkoStDev)'s [ngrx-signal-store-playground](https://github.com/markostanimirovic/ngrx-signal-store-playground/tree/main/src/app/shared) repo, and
* the source code of the [@ngrx/signals/entities](https://ngrx.io/guide/signals/signal-store/entity-management) package

The full source code is available here:



* Demo app: [https://github.com/gergelyszerovay/store-extensions/tree/main/projects/demo/src/app/article-list-ngrx-signal-store-feature](https://github.com/gergelyszerovay/store-extensions/tree/main/projects/demo/src/app/article-list-ngrx-signal-store-feature) 
* <code>withDataService: </code>[https://github.com/gergelyszerovay/store-extensions/tree/main/projects/signal-store-data-service-feature](https://github.com/gergelyszerovay/store-extensions/tree/main/projects/signal-store-data-service-feature) 

To start the demo app, clone [https://github.com/gergelyszerovay/store-extensions/](https://github.com/gergelyszerovay/store-extensions/), run <code>pnpm install</code> and <code>pnpm run start</code>,  then open [http://localhost:4200/article-list-signal-store-with-feature](http://localhost:4200/article-list-signal-store-with-feature) in your browser.


## Demo application architecture and features

The demo application has the following features:



* A simple menu to switch between the different article list implementations (`UiArticleListComponent`)
* The `ArticleListComponent_SSF` is a smart component, it provides and uses the `ArticleListSignalStoreWithFeature` store. The component loads the article list from the server, so it has a loading and an error state. It supports pagination, and the user can also change the pagination by URL parameters, for example: [http://localhost:4200/article-list-signal-store-with-feature?selectedPage=3&pageSize=2](http://localhost:4200/article-list-signal-store-with-feature?selectedPage=3&pageSize=2). If the user changes the URL parameters or clicks on the pagination component, the article list gets reloaded.

The `ArticleListComponent_SSF` component has two child UI component:



* An article list component (`UiArticleListComponent`), it shows the article list with the article’s author, publication date, like count, tags and lead. 
* A pagination component below the article list (`UiPaginationComponent`).


## The store

This is the state for the `ArticleListComponent_SSF`:


```ts
type ArticleListState = {
  readonly selectedPage: number,
  readonly pageSize: number,
  readonly articlesCount: number
}

export const initialArticleListState: ArticleListState = {
  selectedPage: 0,
  pageSize: 3,
  articlesCount: 0
}
```


And this is the `SignalStore`:


```ts
export const ArticleListSignalStoreWithFeature = signalStore(
  withState(initialArticleListState),
  withEntities({ entity: type<Article>(), collection: 'article' }),
  withComputed(({ articlesCount, pageSize }) => ({
    totalPages: computed(() => Math.ceil(articlesCount() / pageSize())),
  })),
  withComputed(({ selectedPage, totalPages }) => ({
    pagination: computed(() => ({ selectedPage: selectedPage(), totalPages: totalPages() })),
  })),
  withMethods((store) => ({
    setSelectedPage(selectedPage: string | number | undefined): void {
      patchState(store, () => ({
        selectedPage: selectedPage === undefined ? initialArticleListState.selectedPage : Number(selectedPage),
      }));
    },
    setPageSize(pageSize: string | number | undefined): void {
      patchState(store, () => ({
        pageSize: pageSize === undefined ? initialArticleListState.pageSize : Number(pageSize)
      }));
    },
  })),
  withDataService({
    actionName: 'loadArticles',
    service: (store /*, rxParams: void*/) => {
      const articlesService = inject(ArticlesService);
      return articlesService.getArticles({
        limit: store.pageSize(),
        offset: store.selectedPage() * store.pageSize()
      })
      .pipe(map(response => {
        return [
          // setAllEntities doesn't work with readonly arrays, ReadonlyArray<Article> => Array<Article>
          setAllEntities(response.articles as Array<Article>, { collection: 'article' }),
          {
            articlesCount: response.articlesCount
          }
      ] }))
    }
  }),
  withDataService({
    actionName: 'toggleFavorite',
    service: (store, articleId: number) => {
      const articlesService = inject(ArticlesService);
      const article = store.articleEntityMap()[articleId]!;
      console.log('optimistic update', article);
      if (article.favorited) {
        patchState(store, setEntity(
          { ...article, favorited: false, favoritesCount: article.favoritesCount - 1 },
          { collection: 'article' })
        );
      }
      else {
        patchState(store, setEntity(
          { ...article, favorited: true, favoritesCount: article.favoritesCount + 1 },
          { collection: 'article' })
        );
      }
      // send the request to the server
      return articlesService.toggleFavorite(articleId).pipe(
      // transform the response to the store's data format
      map(response => {
        return [
          setEntity(response, { collection: 'article' })
      ] }));
    }
  })
);
```


This store has the following properties and methods:



* `withState `adds the following signals: `selectedPage`, `pageSize`, `articlesCount,` these contain the pagination data and the total number of the articles
* `withEntities` (collection: 'article') adds the `articleEntityMap `and `articleEntityIds `signals, these contain the article entities. It adds the   `articleEntities `computed signal with the list of the articles, too.
* `withComputed`s add two computed signals: `totalPages `and `pagination,` these are the inputs of the pagination component
* `withMethods` adds to method to update the selected page and the page size in the store
* `withDataService` (actionName: 'loadArticles') adds the `loadArticles()` RxMethod, the `loadArticlesRequestState` signal, and the `isArticleListEmpty, isArticleListFetching, isArticleListFetched` and `getArticleListError` computed signals to the store
* `withDataService` (actionName: 'toggleFavorite') adds the `toggleFavorite()` RxMethod, the `toggleFavoriteRequestState` signal, and the `isToggleFavoriteEmpty, isToggleFavoriteFetching, isToggleFavoriteFetched` and `getToggleFavoriteError` computed signals to the store


## The `withDataService` Custom Store Feature

The `withDataService` feature connects a data service to the store and tracks the state of the HTTP request.


### HTTP Request state

The `withDataService` feature represents the request state with the `HttpRequestState` data type:


```ts
export type HttpRequestState = HttpRequestStates | HttpRequestError;
```


The `HttpRequestState`  can be one of these request states:


```ts
export enum HttpRequestStates {
  // no request has been made
  INITIAL = 'INITIAL', 
  // a request is started, and we're waiting for the server's response
  FETCHING = 'FETCHING', 
  // a request has been successfully fetched
  FETCHED ='FETCHED' 
}
```


or a `HttpRequestError` object, if the request fails:


```ts
export type HttpRequestError = {
  readonly errorMessage: string,
  readonly errorCode?: number
  readonly errorData?: unknown;
}
```


The `withDataService` feature has three config options: `actionName`, `service` and the optional `extractHttpErrorMessageFn`.


### actionName

We use this string to customize the name of the generated signals and methods. If the `actionName `is `loadArticles`, `withDataService` adds the `loadArticles()` RxMethod to the store. In the demo app this RxMethod triggers the loading of the article list. It also adds the `loadArticlesRequestState: HttpRequestState` signal, and the following computed signals:



* `isArticleListInitial()`: true, if the there was no request yet (initial state)
* `isArticleListFetching()`: true, when the service sent the request to the server, but there has been is no response yet
* `isArticleListFetched()`: true, when the service  sent the request to the server, and we got a valid response
* `getArticleListError()`: undefined, when the service sent the request to the server, and we got a valid response. It's a HttpRequestError, if there was an error during the request


### service

This option passes a callback function to `withDataService`.The callback function returns an observable that, when subscribed, executes a request on the server. The function has two parameters:



* `store`: it contains the SignalStore itself, and
* `rxParams`: the parameter we passed to the RxMethod (`loadArticles()` or `toggleFavorite()`). In the `loadArticles()` method we don't use this parameter. We pass the article id to the `toggleFavorite()` method.

The the callback function for `loadArticle` is the following`:`


```ts
    service: (store /*, rxParams: void*/) => {
      const articlesService = inject(ArticlesService);
      return articlesService.getArticles({
        limit: store.pageSize(),
        offset: store.selectedPage() * store.pageSize()
      })
      .pipe(map(response => {
        return [
          // setAllEntities doesn't work with readonly arrays, ReadonlyArray<Article> => Array<Article>
          setAllEntities(response.articles as Array<Article>, { collection: 'article' }),
          {
            articlesCount: response.articlesCount
            }
        ] })
      );
    }
```


It injects the service, creates the observable, and maps the response to a list of partial states (`articlesCount: response.articlesCount`) or partial state updaters (`setAllEntities()`) . This data structure is similar to the parameters of `patchState`. 

The callback function for `toggleFavorite` also implements the optimistic update pattern. It immediately updates the state when called, and when a response is received from the server, it updates the state a second time:


```ts
   service: (store, articleId: number) => {
      // inject the service
      const articlesService = inject(ArticlesService);
      // optimistic update
      const article = store.articleEntityMap()[articleId]!;
      console.log('optimistic update', article);
      if (article.favorited) {
        patchState(store, setEntity(
          { ...article, favorited: false, favoritesCount: article.favoritesCount - 1 },
          { collection: 'article' })
        );
      }
      else {
        patchState(store, setEntity(
          { ...article, favorited: true, favoritesCount: article.favoritesCount + 1 },
          { collection: 'article' })
        );
      }
      // get the observable for sending the request to the server
      return articlesService.toggleFavorite(articleId).pipe(
      // transform the response to the store's data format
      map(response => {
        return [
          setEntity(response, { collection: 'article' })
      ] }));
    }
```



### extractHttpErrorMessageFn

This option is optional. We can specify a function that maps Angular's `HttpErrorResponse` to a `HttpRequestError`. If this function is not specified, `withDataService` uses a simple built-in version. Alternatively, you can specify a customized one that properly handles your specific backend's error responses.


## The article list component

It's a smart component:



* it provides and injects the store, and
* has an effect to update the parameters from the URL and load the article list


```ts
@Component({
  providers: [ArticleListSignalStoreWithFeature],
  template: `
<h1 class="text-xl font-semibold my-4">SignalStore with a feature</h1>
@if (store.isLoadArticlesInitial() || store.isLoadArticlesFetching()) {
  <div>Loading...</div>
}
@if (store.isLoadArticlesFetched()) {
  <app-ui-article-list
    [articles]="store.articleEntities()"
    (toggleFavorite)="store.toggleFavorite($event)"
  />
  <app-ui-pagination
    [selectedPage]="store.pagination().selectedPage"
    [totalPages]="store.pagination().totalPages"
    (onPageSelected)="store.setSelectedPage($event); store.loadArticles();"
  />
}
@if (store.getLoadArticlesError(); as error) {
  {{ error.errorMessage }}
}`
// ...
})
export class ArticleListComponent_SSF {
  // we get these from the router, as we use withComponentInputBinding()
  selectedPage = input<string | undefined>(undefined);
  pageSize = input<string | undefined>(undefined);

  readonly store = inject(ArticleListSignalStoreWithFeature);

  constructor(
  ) {
    effect(() => {
      // 1️⃣ the effect() tracks this two signals only
      const selectedPage = this.selectedPage();
      const pageSize = this.pageSize();
      // 2️⃣ we wrap the function we want to execute on signal change 
      // with an untracked() function
      untracked(() => { // 👈
        // we don't want to track anything in this block
        this.store.setSelectedPage(selectedPage);
        this.store.setPageSize(pageSize);
        this.store.loadArticles();
      });
      console.log('router input ➡️ store (effect)', selectedPage, pageSize); 
    });
  }
}
```



## Summary

In this article, I demonstrated how powerful the Custom Store Features are: I showed you how we can easily connect a store to a data service using the `withDataService` Custom Store Feature. I hope you have found my tutorial useful!

In my next article, I’m going to explain how to unit test smart components and auto-mock SignalStores.

As always, please let me know if you have some feedback!


## 👨‍💻About the author

My name is [Gergely Szerovay](https://www.linkedin.com/in/gergelyszerovay/), I work as a frontend development chapter lead. Teaching (and learning) Angular is one of my passions. I consume content related to Angular on a daily basis — articles, podcasts, conference talks, you name it.

I created the Angular Addict Newsletter so that I can send you the best resources I come across each month. Whether you are a seasoned Angular Addict or a beginner, I got you covered.

Next to the newsletter, I also have a publication called [Angular Addicts](https://angularaddicts.com/). It is a collection of the resources I find most informative and interesting. Let me know if you would like to be included as a writer.

Let’s learn Angular together! [Subscribe here](https://www.angularaddicts.com/) 🔥

Follow me on [Substack](https://www.angularaddicts.com/), [Medium](https://medium.com/@GergelySzerovay), [Dev.to](https://dev.to/gergelyszerovay), [Twitter](https://twitter.com/GergelySzerovay) or [LinkedIn](https://www.linkedin.com/in/gergelyszerovay/) to learn more about Angular!