From NgRx ComponentStore to SignalStore: the key takeaways from my demo project
Signals will change Angular for the better, How to prepare for migration effectively
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:
“Signals in Angular – How to Write More Reactive Code” by Deborah Kurata
“Angular & signals. Everything you need to know” by Robin Goetz
The NgRx team and Marko Stanimirović opened a new RFC (Request for Comments) for a signal-based state management solution, SignalStore. It has a similar approach to @ngrx/component-store
. The initial prototype with the API documentation is available in the NgRx SignalStore playground repo.
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
The application uses the styling and the public hosted backend from the RealWorld project.
The application has the following features:
A simple menu to switch between the
ComponentStore
- and aSignalStore
-based article listTwo article lists, one of them is
ComponentStore
-based, the other one isSignalStore
-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 stateA 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 theComponentStore
-based version of the article list. It’s connected with theArticleListComponentStore
.The
ArticleListComponent_SS
is theSignalStore
-based version of the article list. It’s connected with theArticleListSignalStore
.
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
:
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();
});
}
}
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 theComponentStore
, andwe simply get the value of the signals in the
SignalStore
@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>
`
})
@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):
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:
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:
export const initialArticleListState: ArticleListState = {
selectedPage: 0,
pageSize: 3,
httpRequestState: ‘EMPTY’,
articles: [],
articlesCount: 0
}
Stores
I extend the ArticleListComponentStore
from a ComponentStore
:
@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:
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:
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()
andArticleListSignalStore.articlesCount()
I create an additional combined selector in ArticleListComponentStore
to calculate the number of the pages:
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:
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
:
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:
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:
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:
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:
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
:
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
:
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:
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
andSignalStore
supports RxJs-based effects, additionallySignalStore
supportsPromise
-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 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, 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. 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 🔥
Follow me on Medium, Twitter, Dev.to or LinkedIn to learn more about Angular!