Understanding the demo application setup
To compare the two state management approaches in practice, I built a demo app with two variants of the same "article list" feature. One variant relies on ComponentStore, the other uses SignalStore. The goal was to keep everything else identical and observe what changes when swapping the state layer.
Before diving into code, let me clarify what Angular Signals are. Introduced in Angular 16, Signals provide a new way to track state changes and trigger efficient template updates. They give you fine-grained reactivity, so the framework only re-renders the parts of the UI that actually depend on changed state. If you haven't used Signals yet, I recommend these resources:
- Official Angular Signals documentation
- "Signals in Angular – How to Write More Reactive Code" by Deborah Kurata
- "Angular & signals. Everything you need to know" by Robin Goetz
On the state management side, the NgRx team — with Marko Stanimirović — published an RFC for a signal-based store called SignalStore. The approach is conceptually close to @ngrx/component-store, but built on Signals. You can inspect the playground repository for the prototype API.
In my demo, I first implemented the article list with ComponentStore, then migrated it to SignalStore. Walking through that migration step by step reveals the practical differences between the two.
Full source code:
https://github.com/gergelyszerovay/component-store-to-signal-store
The demo uses the styling and publicly hosted API from the RealWorld project.
The application includes the following capabilities:
- A menu for switching between the
ComponentStore- andSignalStore-based article list -
Two article lists — one backed by
ComponentStore, one bySignalStore. Each list displays the article’s author, publication date, like count, tags and lead. Both handle loading from the server and expose loading and error states - A pagination component below each list. Pagination can also be driven through URL parameters, for example:
http://localhost:4200/article-list-component-store?selectedPage=3&pageSize=2. Changing the URL or clicking the pagination triggers a fresh load of the article list.
How the app is structured
I used Angular v16 with standalone components. Since Signals don't yet support zoneless applications, I applied the OnPush change detection strategy together with async pipes.
The application bootstraps an AppComponent that holds a router-outlet and menu entries pointing to the two variations of the article list:
ArticleListComponent_CS— theComponentStore-based variant, wired to theArticleListComponentStore.ArticleListComponent_SS— theSignalStore-based variant, wired to theArticleListSignalStore.
Both article list features make use of a component-level store and rely on these shared UI pieces:
UiArticleListComponentrenders the collection of articles (each item is aUiArticleLisItemComponent)UiPaginationComponentis responsible for pagination control
The folder layout looks like this:
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/
Inside the article list components
The class definitions for the two article list components are nearly the same:
- we inject the router and the store,
- once the component is initialized, we push the pagination parameters into the store. The same update happens whenever the URL parameters change
The sole distinction lies in the type of the injected store — ArticleListComponentStore versus 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 are also very alike. The important difference is in how we retrieve data from the stores:
- with
ComponentStore, we consume selectors throughasyncpipes, and - with
SignalStore, we read the signals directly
@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
Both stores rely on the same immutable data structure to hold their state. The types HttpRequestState and Articles are also immutable.
export type ArticleListState = {
readonly selectedPage: number,
readonly pageSize: number,
readonly httpRequestState: HttpRequestState,
readonly articles: Articles,
readonly articlesCount: number,
}
The selectedPage property tracks which page is currently shown, while pageSize determines the number of articles displayed per page. Users can modify these values through the pagination component or by passing URL parameters.
The httpRequestState property keeps track of the request status for the article list.
export type HttpRequestState = DeepReadonly<
'EMPTY' | 'FETCHING' | 'FETCHED' |
{ errorMessage: string }
>;
Its initial value is EMPTY. It transitions to FETCHING when a request is about to be sent to the server. Once the server responds successfully, the value changes to FETCHED. If an error occurs, either from the server or during the request, the state is set to an { errorMessage: string } object containing the error details.
The server response provides both the articles and the total article count, which we store in the articles and articlesCount properties.
Once the article list components are instantiated, their stores start with this default state.
export const initialArticleListState: ArticleListState = {
selectedPage: 0,
pageSize: 3,
httpRequestState: ‘EMPTY’,
articles: [],
articlesCount: 0
}
Stores
The ArticleListComponentStore extends the ComponentStore class.
@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) => /* ... */);
}
For the ArticleListSignalStore, I use the signalStore() function. This function takes a series of store features as its arguments, which I will elaborate on below.
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
The ArticleListComponentStore holds its state in the store$ subject. This subject emits a new value each time the state is updated. To monitor individual state properties independently, a dedicated selector is created for each one.
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);
In contrast, SignalStore automatically generates a separate signal for every root-level property of the state, known as partial states. These partial states can be accessed directly.
ArticleListSignalStore.selectedPage()ArticleListSignalStore.pageSize()ArticleListSignalStore.httpRequestState()ArticleListSignalStore.articles()ArticleListSignalStore.articlesCount()
To determine the total number of pages, I add a combined selector in ArticleListComponentStore.
readonly totalPages$: Observable<number> = this.select(
this.articlesCount$, this.pageSize$,
(articlesCount, pageSize) => Math.ceil(articlesCount / pageSize));
To achieve the same result in the ArticleListSignalStore, I use the withComputed() function. This function receives the articlesCount and pageSize signals to calculate the page count.
withComputed(({ articlesCount, pageSize }) => ({
totalPages: computed(() => Math.ceil(articlesCount() / pageSize())),
})),
The pagination component requires a specific “view model” selector. Below is the implementation for this selector in the ArticleListComponentStore.
readonly pagination$: Observable<{ selectedPage: number, totalPages: number }> = this.select(
this.selectedPage$,
this.totalPages$,
(selectedPage, totalPages) => ({ selectedPage, totalPages })
);
The equivalent selector is set up in the ArticleListSignalStore as well.
withComputed(({ selectedPage, totalPages }) => ({
pagination: computed(() => ({ selectedPage, totalPages })),
})),
Updaters
When working with updaters in a ComponentStore, the pattern consistently involves constructing a fresh immutable state object that carries the revised values, then returning it. Every property from the existing state—both those that changed and those that remain untouched—must be included in this returned object.
Here’s an example of processing a server response:
setRequestStateSuccess = this.updater((state, params: ArticlesResponseType): ArticleListState => {
return {
...state,
httpRequestState: ‘FETCHED’,
articles: params.articles,
articlesCount: params.articlesCount
}
});
The params argument holds the articles and articlesCount data returned from the server:
export type ArticlesResponseType = {
articles: Articles,
articlesCount: number
}
For the ArticleListSignalStore, updaters are defined using the withUpdaters() function. Inside these updaters, only the modified properties are used to build a new immutable object—there’s no spread of the entire state here. The SignalStore merges these returned partial state values into the overall state:
withUpdaters(({ update }) => ({
setPaginationSettings: (s: RouteParamsPaginatonState) => update(() => ({
// ...
setRequestStateSuccess: (params: ArticlesResponseType) => update(() => ({
httpRequestState: ‘FETCHED’,
articles: params.articles,
articlesCount: params.articlesCount
}))
// ...
}))
Effects
Both stores rely on a single effect responsible for retrieving the article list from the server. Below is the effect belonging to 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, effects are set up through the withEffects() function. There are two distinct effect flavors supported: those based on RxJs and those built around Promise. The RxJs-powered effects bear a strong resemblance to what you’d write 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’);
}
)
)
)
}
}
)
When Promise-based effects come into play, they’re handy in situations where a Promise offers all the functionality required, making the full capabilities of RxJs unnecessary. However, when it comes to fetching data from a server, there’s a notable limitation: there’s no built-in support for 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 wrap things up, here are the primary distinctions between ComponentStore and SignalStore:
-
ComponentStoremaintains its state within thestate$subject, whereasSignalStorekeeps a dedicated signal for each top-level property of the state—these are referred to as partial states. - For a
SignalStore, accessing root-level state properties doesn’t require selectors. Each of these properties lives in its own signal, offering direct access without any extra steps. - While both
ComponentStoreandSignalStoreaccommodate RxJs-driven effects,SignalStoreextends functionality by also supporting effects based onPromise.
Even though the SignalStore implementation currently stands as a prototype—and it’s plausible that API adjustments lie ahead—I genuinely find pleasure in working with it. The API feels flexible and intuitive, building on the foundational ideas of the ComponentStore but offering a more refined experience.
To enhance the debugging experience around state changes, updaters, and effects, I incorporated some diagnostic code into the original SignalStore, borrowed from my ngx-ngrx-component-store-debug-tools project.
The pressing consideration for me now revolves around structuring ComponentStores in a manner that paves the way for a smooth migration once the production-ready SignalStore ships.
In the upcoming segment of this article series, I’ll lay out a set of guidelines aimed at achieving that objective. I’ll also explore more intricate scenarios to compare the two approaches—covering topics such as how HTTP request cancellation plays out with both SignalStore and ComponentStore.
I appreciate you taking the time to read through this. If you have thoughts or suggestions, feel free to share them—your feedback is always welcome.
👨💻About the author
I’m Gergely Szerovay, serving as a frontend development chapter lead. My enthusiasm lies in both teaching and learning Angular—a passion I pursue daily through articles, podcasts, conference talks, and any other resource I can get my hands on.
I founded the Angular Addict Newsletter with the aim of curating and sharing top-notch resources with you every month, whether you’re just getting started or you’re a seasoned Angular devotee.
In addition to the newsletter, I run a publication fittingly named Angular Addicts. It serves as a hub for the materials I find most valuable and engaging. If you’re interested in contributing as a writer, don’t hesitate to reach out.
Let’s dive into Angular together! Subscribe here 🔥
For more Angular insights, connect with me on Medium, Twitter, or LinkedIn.

