From NgRx ComponentStore to SignalStore: the key takeaways from my demo project
Tags: angular, ngrx, typescript, javascript
Published: 2023-05-16 07:59:49

---
title: From NgRx ComponentStore to SignalStore: the key takeaways from my demo project
published: true
description: Signals will change Angular for the better, How to prepare for migration effectively
tags: #angular #ngrx #typescript #javascript
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nsm2hk7259rdyo0vmuqd.png
# Use a ratio of 100:42 for best results.
# published_at: 2023-05-07 16:38 +0000
canonical_url: https://www.angularaddicts.com/p/from-ngrx-componentstore-to-signalstore
---

I believe that Signals in Angular will fundamentally change the way we create Angular applications. This article is the first part of a series that aims to show you the potential of this new feature, and at the same time help you prepare for this change effectively: while Signals is in developer preview and the NgRx signal-based store is just a prototype, you can start creating and refactoring your components in a way that will make migration really smooth for you. In this first part, I show you how I used a demo application to showcase the differences between the `ComponentStore` and the new signal-based model. In the next part of the series, I’m going to offer you some guidelines on how to navigate this change. So first let me introduce **Signals** and the **NgRx SignalStore**.

**Angular Signals** is a new reactivity model in Angular 16. **Signals** help us **track state changes** in our applications and trigger **optimized template rendering** updates. If you are new to Signals, here are some highly recommended articles:

- [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
- [“Angular & signals. Everything you need to know”](https://dev.to/this-is-angular/angular-signals-everything-you-need-to-know-2b7g) by Robin Goetz

The NgRx team and [Marko Stanimirović](https://twitter.com/MarkoStDev) opened a [new RFC (Request for Comments) for a signal-based state management solution, SignalStore](https://github.com/ngrx/platform/discussions/3796). It has a similar approach to `@ngrx/component-store`. The initial prototype with the API documentation is available in the [NgRx SignalStore playground repo](https://github.com/markostanimirovic/ngrx-signal-store-playground).

As I mentioned, I'm confident that Signals will transform the way we develop Angular applications. To gain more knowledge of this new feature and its future impacts, I’ve created two versions of an “article list” component. I’ve built a `ComponentStore`-based one first, then migrated it to a `SignalStore`-based one. In this article, I explain the implementation steps and the main differences I found, so that you can better understand how `SignalStore`s actually work.

**The full source code is available here:**

[https://github.com/gergelyszerovay/component-store-to-signal-store](https://github.com/gergelyszerovay/component-store-to-signal-store)

The application uses the styling and the public hosted backend from the [RealWorld project](https://realworld-docs.netlify.app/docs/specs/frontend-specs/api/#demo-api).

![Screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8dvojb3da2qmkpnc3fwj.png)

The application has the **following features**:

- A simple **menu** to switch between the `ComponentStore`- and a `SignalStore`-based article list
- **Two article lists**, one of them is `ComponentStore`-based, the other one is `SignalStore`-based. They show the article’s author, publication date, like count, tags and lead. They load the article list from the server, so they have a loading and an error state
- A **pagination** component below each article list. The user can also change the pagination by URL parameters, for example: `http://localhost:4200/article-list-component-store?selectedPage=3&pageSize=2`. If the user changes the URL parameters or clicks on the pagination component, the article list gets reloaded.

## Application architecture

I use Angular v16 with standalone components. As Signals doesn’t work in zoneless applications yet, I use the `OnPush` change detection strategy with `async` pipes.

The app bootstraps an `AppComponent` with a `router-outlet` and two menu items for the two versions of the article list:

- The `ArticleListComponent_CS` is the `ComponentStore`-based version of the article list. It’s connected with the `ArticleListComponentStore`.
- The `ArticleListComponent_SS` is the `SignalStore`-based version of the article list. It’s connected with the `ArticleListSignalStore`.

Both “article list“ implementations use a component-level store and rely on the following UI components:

- The `UiArticleListComponent` renders the list of the articles (`UiArticleLisItemComponent`)
- The `UiPaginationComponent` handles the pagination

The directory structure is the following:

```
src/
|-- app/
|   |-- article-list-ngrx-component-store/ => ArticleListComponent_CS
|   |-- article-list-ngrx-signal-store/ => ArticleListComponent_SS
|   |-- models/
|   |-- services/
|   |-- ui-components/ 
|   |-- app.component.ts
|   |-- app.routes.ts
|-- libs/signal-store/
```

## The article list components

The class codes of the two article list components are almost identical:

- we inject the router and the store,
- we update the pagination parameters in the store after the component was created. We also update the parameters if the parameters on the URL change

The only difference between them is the class of the injected store: `ArticleListComponentStore` and `ArticleListSignalStore`:

```ts
export class ArticleListComponent_CS {
  readonly store = inject(ArticleListComponentStore);
  readonly route = inject(ActivatedRoute);

  constructor(
  ) {
    this.route.queryParams.pipe(takeUntilDestroyed()).subscribe(
      routeParams => {
      this.store.setPaginationSettings(routeParams);
      this.store.loadArticles();
    });
  }
}
```

```ts
export class ArticleListComponent_SS {
  readonly store = inject(ArticleListSignalStore);
  readonly route = inject(ActivatedRoute);

  constructor(
  ) {
    this.route.queryParams.pipe(takeUntilDestroyed()).subscribe(
      routeParams => {
      this.store.setPaginationSettings(routeParams);
      this.store.loadArticles();
    });
  }
}
```

The templates of the components are similar, too. The fundamental difference is the way we read the data from the stores:

- we use `async` pipes to read from the **selectors** of the `ComponentStore`, and
- we simply get the value of the **signals** in the `SignalStore`

```ts
@Component({
  selector: ‘app-article-list-cs’,
  // ...
  providers: [ArticleListComponentStore],
  template: `
<ng-container *ngIf="(store.httpRequestState$ | async) === ‘FETCHING’">
  Loading...
</ng-container>
<ng-container *ngIf="store.httpRequestState$ | async | httpRequestStateErrorPipe as errorMessage">
  {{ errorMessage }}
</ng-container>
<ng-container *ngIf="(store.httpRequestState$ | async) === ‘FETCHED’">
  <ng-container *ngIf="store.articles$ | async as articles">
    <app-ui-article-list [articles]="articles"/>
  </ng-container>
  <ng-container *ngIf="store.pagination$ | async as pagination">
    <app-ui-pagination
      [selectedPage]="pagination.selectedPage"
      [totalPages]="pagination.totalPages"
      (onPageSelected)="store.setSelectedPage($event); store.loadArticles();" />
  </ng-container>
</ng-container>
  `
})
```

```ts
@Component({
  selector: ‘app-article-list-ss’,
  // ...
  providers: [ArticleListSignalStore],
  template: `
<ng-container *ngIf="store.httpRequestState() === ‘FETCHING’">
  Loading...
</ng-container>
<ng-container *ngIf="store.httpRequestState() | httpRequestStateErrorPipe as errorMessage">
  {{ errorMessage }}
</ng-container>
<ng-container *ngIf="store.httpRequestState() === ‘FETCHED’">
  <ng-container *ngIf="store.articles() as articles">
    <app-ui-article-list [articles]="articles"/>
  </ng-container>
  <ng-container *ngIf="store.pagination() as pagination">
    <app-ui-pagination
      [selectedPage]="pagination.selectedPage()"
      [totalPages]="pagination.totalPages()"
      (onPageSelected)="store.setSelectedPage($event); store.loadArticles();" />
  </ng-container>
</ng-container>
  `
})
```

## State

I apply the same immutable data structure for storing the state in both stores (`HttpRequestState` and `Articles` are also immutable types):

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

  readonly httpRequestState: HttpRequestState,

  readonly articles: Articles,
  readonly articlesCount: number,
}
```

The `selectedPage` property specifies the currently visible page, the `pageSize` property defines how many articles are visible. The user can change these values by using the pagination component or by applying URL parameters.

The `httpRequestState` property contains the request state of the article list:

```ts
export type HttpRequestState = DeepReadonly<
  'EMPTY' | 'FETCHING' | 'FETCHED' |
  { errorMessage: string }
  >;
```

Initially, its value is `EMPTY`. We change it to `FETCHING` right before we send a request to the server. When the server’s response arrives, we set its value to `FETCHED`. If the server sends an error response or there is an error during the request, we set the request state to an `{ errorMessage: string }` object with the error message.

The server’s response contains the total number of articles and the articles themselves, we store these in the `articlesCount` and `articles` properties.

After we create the article list components, their stores have an initial state:

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

  httpRequestState: ‘EMPTY’,

  articles: [],
  articlesCount: 0
}
```

## Stores

I extend the `ArticleListComponentStore` from a `ComponentStore`:

```ts
@Injectable()
export class ArticleListComponentStore extends ComponentStore<ArticleListState> {
  readonly selectedPage$: Observable<number> = /* ... */;
  readonly pageSize$: Observable<number> = /* ... */;
  readonly httpRequestState$: Observable<HttpRequestState> = /* ... */;
  readonly articles$: Observable<DeepReadonly<Articles>> = /* ... */;
  readonly articlesCount$: Observable<number> = /* ... */;

  readonly totalPages$: Observable<number> = /* ... */;

  readonly pagination$: Observable<{ selectedPage: number, totalPages: number }> = /* ... */;

  readonly articlesService = inject(ArticlesService);

  constructor(
  ) {
    super(initialArticleListState);
  }

  setPaginationSettings = this.updater(
    (state, s: RouteParamsPaginatonState) => /* ... */);

  readonly loadArticles = this.effect<void>(/* ... */);

  setRequestStateLoading = this.updater(
    (state) => /* ... */);

  setRequestStateSuccess = this.updater(
    (state, params: ArticlesResponseType) => /* ... */);

  setRequestStateError = this.updater(
    (state, error: string): => /* ... */);

  setSelectedPage = this.updater(
    (state, selectedPage: number) => /* ... */);
}
```

I create the `ArticleListSignalStore` with the `signalStore()` function. It accepts a sequence of store features, I’ll explain these in more detail:

```ts
export const ArticleListSignalStore = signalStore(
  { debugId: ‘ArticleListSignalStore’ },
  withState<ArticleListState>(initialArticleListState),
  withComputed(({ articlesCount, pageSize }) => ({ /* ... */ })),
  withComputed(({ selectedPage, totalPages }) => ({ /* ... */ })),
  withUpdaters(({ update }) => ({
    setPaginationSettings: (s: RouteParamsPaginatonState) =>  /* ... */,
    setRequestStateLoading: () => /* ... */ ,
    setRequestStateSuccess: => /* ... */ ,
    setRequestStateError: (error: string) => /* ... */ ,
    setSelectedPage: (selectedPage: number) =>  /* ... */,
  withEffects(
    ( {
      selectedPage, pageSize,
      setRequestStateLoading, setRequestStateSuccess, setRequestStateError
      },
    ) => {
      const articlesService = inject(ArticlesService)
      // ...
    }
  )
);
```

## Selectors

`ArticleListComponentStore` stores the state in its `store$` subject. This subject emits a value on every state change. To observe the modifications of the state’s properties individually, we make a separate selector for each of these properties:

```ts
 readonly selectedPage$: Observable<number> = 
    this.select(state => state.selectedPage);
  readonly pageSize$: Observable<number> = 
    this.select(state => state.pageSize);
  readonly httpRequestState$: Observable<HttpRequestState> = 
    this.select(state => state.httpRequestState);
  readonly articles$: Observable<DeepReadonly<Articles>> = 
    this.select(state => state.articles);
  readonly articlesCount$: Observable<number> = 
    this.select(state => state.articlesCount);
```

`SignalStore` automatically creates a separate `signal` for all the root properties of the state. We refer to these as partial states. We can access these partial states by:

- `ArticleListSignalStore.selectedPage()`
- `ArticleListSignalStore.pageSize()`
- `ArticleListSignalStore.httpRequestState()`
- `ArticleListSignalStore.articles()` and
- `ArticleListSignalStore.articlesCount()`

I create an additional combined selector in `ArticleListComponentStore` to calculate the number of the pages:

```ts
 readonly totalPages$: Observable<number> = this.select(
    this.articlesCount$, this.pageSize$,
    (articlesCount, pageSize) => Math.ceil(articlesCount / pageSize));
```

To do the same in the `ArticleListSignalStore`, I use the `withComputed()` function. I provide the `articlesCount` and `pageSize` signals as a parameter to the function, and calculate the total number of the pages:

```ts
 withComputed(({ articlesCount, pageSize }) => ({
    totalPages: computed(() => Math.ceil(articlesCount() / pageSize())),
  })),
```

We also need to add a “view model” selector to the pagination component. This is the code for the selector in `ArticleListComponentStore`:

```ts
 readonly pagination$: Observable<{ selectedPage: number, totalPages: number }> = this.select(
    this.selectedPage$,
    this.totalPages$,
    (selectedPage, totalPages) => ({ selectedPage, totalPages })
  );
```

And this is the same selector in the `ArticleListSignalStore`, too:

```ts
 withComputed(({ selectedPage, totalPages }) => ({
    pagination: computed(() => ({ selectedPage, totalPages })),
  })),
```

## Updaters

Inside the updaters of a `ComponentStore`, we always create a new immutable state object with the updated values and return it. The returned state object contains all properties from the state, both the updated and the unmodified ones.

For example, this is how we handle a server response:

```ts
 setRequestStateSuccess = this.updater((state, params: ArticlesResponseType): ArticleListState => {
    return {
      ...state,
      httpRequestState: ‘FETCHED’,
      articles: params.articles,
      articlesCount: params.articlesCount
    }
  });
```

The `params` parameter contains the articles and `articlesCount` values from the server’s response:

```ts
export type ArticlesResponseType = {
  articles: Articles,
  articlesCount: number
}
```

In the `ArticleListSignalStore`, we create the `updater`s with the `withUpdaters()` function. In these updaters, we create a new immutable object from the updated properties only, so there is no `...state` here. The `SignalStore` updates the partial states with using these returned property values:

```ts
 withUpdaters(({ update }) => ({
    setPaginationSettings: (s: RouteParamsPaginatonState) => update(() => ({
    // ...
    setRequestStateSuccess: (params: ArticlesResponseType) => update(() => ({
      httpRequestState: ‘FETCHED’,
      articles: params.articles,
      articlesCount: params.articlesCount
    }))
    // ...
  }))
```

## Effects

The stores have a single `effect` that fetches the article list from the server. This is the `effect` of `ArticleListComponentStore`:

```ts
 readonly loadArticles = this.effect<void>((trigger$: Observable<void>) => {
    return trigger$.pipe(
      withLatestFrom(this.selectedPage$, this.pageSize$),
      tap(() => this.setRequestStateLoading()),
      switchMap(([, selectedPage, pageSize]) => {
        return this.articlesService.getArticles({
          limit: pageSize,
          offset: selectedPage * pageSize
        }).pipe(
          tapResponse(
            (response) => {
              this.setRequestStateSuccess(response);
            },
            (errorResponse: HttpErrorResponse) => {
              this.setRequestStateError(‘Request error’);
            }
          ),
        );
      }),
    );
  });
```

In a `SignalStore`, we implement effects with the `withEffects()` function. `SignalStores` support two different effect types: RxJs-based effects and `Promise`-based effects. The RxJs-based effects look very similar to the effects we use in a `ComponentStore`:

```ts
withEffects(
    ( {
      selectedPage, pageSize,
      setRequestStateLoading, setRequestStateSuccess, setRequestStateError
      },
    ) => {
      const articlesService = inject(ArticlesService)
      return {
        loadArticles: rxEffect<void>(
          pipe(
            tap(() => setRequestStateLoading()),
            switchMap(() => articlesService.getArticles({
              limit: pageSize(),
              offset: selectedPage() * pageSize()
            })),
            tapResponse(
              (response) => {
                setRequestStateSuccess(response);
              },
              (errorResponse: HttpErrorResponse) => {
                setRequestStateError(‘Request error’);
              }
            )
          )
        )
      }
    }
  )
```

`Promise`-based effects are useful when a `Promise` has sufficient functionality and we don’t need the power of RxJs. In case of fetching data from the server, it has a drawback: it doesn’t support a cancellation logic:

```ts
withEffects(
    ( {
      selectedPage, pageSize,
      setRequestStateLoading, setRequestStateSuccess, setRequestStateError
      },
    ) => {
      const articlesService = inject(ArticlesService)
      return {
        async loadArticles() {
          setRequestStateLoading();
          try {
            const response = await lastValueFrom(articlesService.getArticles({
              limit: pageSize(),
              offset: selectedPage() * pageSize()
            }));
            setRequestStateSuccess(response);
          }
          catch(e) {
            setRequestStateError(‘Request error’);
          }
        }
      }
    }
  )
```

## Summary

To sum up, the key differences between the `ComponentStore` and the `SignalStore` are these:

- `ComponentStore` has its state in the state$ subject. `SignalStore` has a separate signal for all the root properties of the state (partial states)
- In a `SignalStore`, we don’t need selectors to access the root level properties of the state. It stores these in separate signals, so these are directly accessible.
- Both `ComponentStore` and `SignalStore` supports RxJs-based effects, additionally `SignalStore` supports `Promise`-based effects, too.

Although the current `SignalStore` implementation is just a prototype, and there might be API changes in the future, I really enjoy working with it. Its API is flexible and easy to understand, as it follows the basic concepts of the `ComponentStore`, but in a more advanced way.

To make it easier to debug state changes, updaters and effects, I patched the original `SignalStore` code with some debug code from my [ngx-ngrx-component-store-debug-tools](https://github.com/gergelyszerovay/ngx-ngrx-component-store-debug-tools) project.

The main question I have now is how to create the `ComponentStores` in a way that when the production-ready `SignalStore` is released, it’ll allow for an easily manageable migration process.

In the next part of my article series, I will define some guidelines that will help us reach this goal. Additionally, I’m going to examine some complex scenarios and compare these two approaches, for example how HTTP request cancellation works with `SignalStore` and `ComponentStore`.

Thanks for reading, I hope you found my article helpful, please let me know if you have some feedback! 

---

## 👨‍💻About the author

My name is [Gergely Szerovay](https://dev.to/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 — you guessed it — [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://newsletter.angularaddicts.com/) 🔥

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