NgRx Signal Store vs Classic NgRx in 2026: Which Should You Choose?
A comprehensive comparison of NgRx Signal Store and Classic NgRx for Angular state management. Learn when to use each approach with practical code examples.

NgRx Signal Store represents a fundamental shift in Angular state management, replacing the traditional Redux-inspired actions-reducers-selectors pattern with a signal-based reactive approach. With NgRx 22 (released August 2026), Signal Store gains resource extensions, dynamic deep signals, and improved composability. This comparison examines both solutions to help Angular developers make informed architectural decisions.
Use Signal Store for feature-level state, medium-complexity apps, and Angular's native Resource API integration. Use Classic NgRx for enterprise applications requiring strict audit trails, complex side effects, and extensive DevTools debugging.
Understanding the Architectural Differences
Classic NgRx follows the Redux pattern: actions describe events, reducers produce new immutable state, and selectors extract data. Every state change flows through a predictable, traceable pipeline.
Signal Store takes a different approach. Instead of dispatching actions through reducers, state lives in reactive signals. Updates happen through methods that call patchState(), and derived values come from computed signals rather than memoized selectors.
| Aspect | Classic NgRx | Signal Store |
|---|---|---|
| Mental Model | Redux/Flux | Reactive Services |
| Boilerplate | High (actions, reducers, selectors, effects) | Low (state, methods, computed) |
| Reactivity | RxJS Observables | Angular Signals |
| Resource API | Manual integration | Native with @ngrx/signals/resource |
| DevTools | Full Redux DevTools | Signal Store DevTools (via plugin) |
| Learning Curve | Steep | Moderate |
| Bundle Size | Larger (~15-20KB gzipped) | Smaller (~4KB gzipped) |
For developers already familiar with Angular Signals, Signal Store feels like a natural extension of the framework's reactive primitives.
Classic NgRx: The Redux Pattern in Angular
Classic NgRx organizes state management around actions, reducers, selectors, and effects. This separation enforces predictability at the cost of verbosity.
import { createActionGroup, emptyProps, props } from '@ngrx/store';
import { Book } from './book.model';
export const BooksActions = createActionGroup({
source: 'Books',
events: {
'Load Books': emptyProps(),
'Load Books Success': props<{ books: Book[] }>(),
'Load Books Failure': props<{ error: string }>(),
'Add Book': props<{ book: Book }>(),
'Remove Book': props<{ id: string }>(),
},
});The createActionGroup API groups related actions under a single source, improving DevTools readability and reducing boilerplate compared to individual createAction calls.
import { createReducer, on } from '@ngrx/store';
import { BooksActions } from './books.actions';
import { Book } from './book.model';
export interface BooksState {
books: Book[];
loading: boolean;
error: string | null;
}
const initialState: BooksState = {
books: [],
loading: false,
error: null,
};
export const booksReducer = createReducer(
initialState,
on(BooksActions.loadBooks, (state) => ({
...state,
loading: true,
error: null,
})),
on(BooksActions.loadBooksSuccess, (state, { books }) => ({
...state,
books,
loading: false,
})),
on(BooksActions.loadBooksFailure, (state, { error }) => ({
...state,
loading: false,
error,
}))
);Reducers remain pure functions that return new state objects. The on() function handles action matching and state updates concisely.
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { BooksState } from './books.reducer';
export const selectBooksState = createFeatureSelector<BooksState>('books');
export const selectAllBooks = createSelector(
selectBooksState,
(state) => state.books
);
export const selectBooksLoading = createSelector(
selectBooksState,
(state) => state.loading
);
export const selectPublishedBooks = createSelector(
selectAllBooks,
(books) => books.filter((book) => book.published)
);Selectors provide memoization automatically. The selectPublishedBooks selector only recomputes when the books array changes, making reads efficient even in frequently-updated stores.
NgRx Signal Store: Signals-First State Management
Signal Store consolidates state, computed values, and methods into a single injectable service. The official NgRx documentation describes it as a "fully-featured state management solution with native support for Angular Signals."
import { computed, inject } from '@angular/core';
import { patchState, signalStore, withComputed, withMethods, withState } from '@ngrx/signals';
import { BooksService } from './books.service';
import { Book } from './book.model';
interface BooksState {
books: Book[];
loading: boolean;
error: string | null;
}
const initialState: BooksState = {
books: [],
loading: false,
error: null,
};
export const BooksStore = signalStore(
{ providedIn: 'root' },
withState(initialState),
withComputed(({ books }) => ({
publishedBooks: computed(() => books().filter((book) => book.published)),
totalBooks: computed(() => books().length),
})),
withMethods((store, booksService = inject(BooksService)) => ({
async loadBooks() {
patchState(store, { loading: true, error: null });
try {
const books = await booksService.getAll();
patchState(store, { books, loading: false });
} catch (error) {
patchState(store, { loading: false, error: 'Failed to load books' });
}
},
addBook(book: Book) {
patchState(store, ({ books }) => ({ books: [...books, book] }));
},
removeBook(id: string) {
patchState(store, ({ books }) => ({
books: books.filter((book) => book.id !== id),
}));
},
}))
);The entire feature's state management fits in one file. State properties become signals automatically, computed signals replace selectors, and methods replace action dispatches.
NgRx 22: Resource Extensions and withProps
NgRx 22 introduces the @ngrx/signals/resource entry point, enabling direct integration with Angular's Resource API. The withProps feature centralizes service injection, while resource extensions handle common async patterns.
import { computed, inject } from '@angular/core';
import { patchState, signalStore, withComputed, withMethods, withProps } from '@ngrx/signals';
import { extendResource, withPreviousValueOnLoading, withValueOnError } from '@ngrx/signals/resource';
import { httpResource } from '@angular/core';
import { BooksService } from './books.service';
import { Book } from './book.model';
export const BooksResourceStore = signalStore(
{ providedIn: 'root' },
// Inject services via withProps (private by convention with underscore)
withProps(() => ({
_booksService: inject(BooksService),
})),
// Set up resources with extensions
withProps((store) => ({
_booksResource: extendResource(
httpResource<Book[]>({
url: '/api/books',
defaultValue: [],
}),
withPreviousValueOnLoading(), // Keeps last value while reloading
withValueOnError([]) // Returns empty array on error
),
})),
// Expose read-only resource to consumers
withProps((store) => ({
booksResource: store._booksResource.asReadonly(),
})),
withComputed((store) => ({
books: computed(() => store.booksResource.value()),
isLoading: computed(() => store.booksResource.isLoading()),
publishedBooks: computed(() =>
store.booksResource.value().filter((book) => book.published)
),
}))
);Resource extensions solve two common problems: flickering during reloads (fixed by withPreviousValueOnLoading) and error state handling (fixed by withValueOnError or withPreviousValueOnError). These extensions compose and can be provided globally via provideResourceExtensions.
Entity Management with withEntities
Both approaches support entity collections, but Signal Store's withEntities feature dramatically reduces code.
import { computed, inject } from '@angular/core';
import { patchState, signalStore, withComputed, withMethods } from '@ngrx/signals';
import {
addEntity,
removeEntity,
setAllEntities,
updateEntity,
withEntities,
} from '@ngrx/signals/entities';
import { BooksService } from './books.service';
import { Book } from './book.model';
export const BooksEntityStore = signalStore(
{ providedIn: 'root' },
withEntities<Book>(),
withComputed(({ entities }) => ({
publishedBooks: computed(() => entities().filter((book) => book.published)),
})),
withMethods((store, booksService = inject(BooksService)) => ({
async loadBooks() {
const books = await booksService.getAll();
patchState(store, setAllEntities(books));
},
addBook(book: Book) {
patchState(store, addEntity(book));
},
updateBook(id: string, changes: Partial<Book>) {
patchState(store, updateEntity({ id, changes }));
},
removeBook(id: string) {
patchState(store, removeEntity(id));
},
}))
);The withEntities feature provides ids, entityMap, and entities signals automatically. Entity updaters like addEntity, removeEntity, and updateEntity handle collection mutations.
Ready to ace your Angular interviews?
Practice with our interactive simulators, flashcards, and technical tests.
Custom Store Features for Reusability
Signal Store's signalStoreFeature function enables cross-cutting concerns to be extracted and reused across stores. NgRx 22 adds SignalStoreFeatureType to simplify type extraction.
import { computed } from '@angular/core';
import { signalStoreFeature, withComputed, withState, SignalStoreFeatureType } from '@ngrx/signals';
type RequestStatus = 'idle' | 'pending' | 'fulfilled' | 'error';
export function withRequestStatus() {
return signalStoreFeature(
withState<{ requestStatus: RequestStatus }>({ requestStatus: 'idle' }),
withComputed(({ requestStatus }) => ({
isPending: computed(() => requestStatus() === 'pending'),
isFulfilled: computed(() => requestStatus() === 'fulfilled'),
isError: computed(() => requestStatus() === 'error'),
}))
);
}
// NgRx 22: extract types without manual redeclaration
export type RequestStatusFeature = SignalStoreFeatureType<typeof withRequestStatus>;
export function setPending(): { requestStatus: RequestStatus } {
return { requestStatus: 'pending' };
}
export function setFulfilled(): { requestStatus: RequestStatus } {
return { requestStatus: 'fulfilled' };
}
export function setError(): { requestStatus: RequestStatus } {
return { requestStatus: 'error' };
}This custom feature can now be composed into any store that needs loading state tracking.
import { inject } from '@angular/core';
import { patchState, signalStore, withMethods } from '@ngrx/signals';
import { setAllEntities, withEntities } from '@ngrx/signals/entities';
import { withRequestStatus, setFulfilled, setPending, setError } from './with-request-status';
import { BooksService } from './books.service';
import { Book } from './book.model';
export const BooksStore = signalStore(
{ providedIn: 'root' },
withEntities<Book>(),
withRequestStatus(),
withMethods((store, booksService = inject(BooksService)) => ({
async loadBooks() {
patchState(store, setPending());
try {
const books = await booksService.getAll();
patchState(store, setAllEntities(books), setFulfilled());
} catch {
patchState(store, setError());
}
},
}))
);The composed store now exposes isPending(), isFulfilled(), and isError() computed signals without duplicating logic.
Synchronous State Tracking with watchState
NgRx 22 adds the watchState function for synchronous state change tracking, useful for undo/redo functionality or state history.
import { inject, DestroyRef } from '@angular/core';
import { watchState } from '@ngrx/signals';
import { BooksStore } from './books.store';
export function setupStateHistory() {
const store = inject(BooksStore);
const destroyRef = inject(DestroyRef);
const history: unknown[] = [];
// Synchronous tracking: captures every state change, no coalescing
watchState(store, (state) => {
history.push(structuredClone(state));
});
return { history };
}Unlike effect-based tracking that coalesces updates, watchState fires synchronously on each patchState call, capturing the complete state history.
Component Integration Patterns
Signal Store integrates naturally with Angular's template syntax through signal reads.
import { Component, inject } from '@angular/core';
import { BooksStore } from './books.store';
@Component({
selector: 'app-books-list',
standalone: true,
template: `
@if (store.isPending()) {
<div class="loading-spinner">Loading books...</div>
}
@if (store.isError()) {
<div class="error-message">Failed to load books</div>
}
@for (book of store.entities(); track book.id) {
<div class="book-card">
<h3>{{ book.title }}</h3>
<p>{{ book.author }}</p>
<button (click)="store.removeBook(book.id)">Remove</button>
</div>
}
<p>Total: {{ store.totalBooks() }} books</p>
`,
})
export class BooksListComponent {
readonly store = inject(BooksStore);
constructor() {
this.store.loadBooks();
}
}No subscription management. No async pipes. Signals integrate with Angular's zoneless change detection for optimal performance. With Angular 22's OnPush as the default change detection strategy, Signal Store components benefit from precise, granular updates.
Performance and Bundle Size Considerations
Signal Store's lighter architecture translates to smaller bundles. The core @ngrx/signals package adds roughly 4KB gzipped, compared to 15-20KB for the full classic NgRx stack (@ngrx/store, @ngrx/effects, @ngrx/entity).
Signal Store also benefits from Angular's signal-based change detection. Computed signals only recalculate when dependencies change, and components only re-render when consumed signals update. This granular reactivity eliminates unnecessary change detection cycles common in Observable-based patterns.
For applications already using RxJS heavily, classic NgRx's Observable-based selectors integrate seamlessly with existing pipelines.
NgRx 22 requires Angular 22.x, TypeScript 6.0.x, and RxJS 6.5.x or 7.5.x. Update with ng update @ngrx/signals@22 for Signal Store projects.
Migration Strategy from Classic to Signal Store
Migrating an existing classic NgRx application doesn't require a big-bang rewrite. Both solutions can coexist, allowing incremental adoption.
- Start with new features using Signal Store
- Migrate isolated feature modules one at a time
- Keep complex, heavily-effectful features on classic NgRx if the migration cost exceeds the benefit
- Use the NgRx Signals interop utilities to bridge between Observable-based and signal-based state
Both classic NgRx Store and Signal Store can run side-by-side in the same application. The NgRx team officially supports both approaches and provides interop utilities for bridging between them.
When to Choose Each Approach
Choose Signal Store when:
- Building new Angular 22+ applications
- Managing feature-level or component-level state
- Integrating with Angular's Resource API and Signal Forms
- Prioritizing developer experience and reduced boilerplate
- The team has limited NgRx experience
- Bundle size matters
Choose Classic NgRx when:
- Maintaining existing NgRx applications
- Requiring strict action logging and audit trails
- Building complex workflows with extensive side effects
- Needing full Redux DevTools time-travel debugging
- The team has deep NgRx expertise
For state management fundamentals, understanding both approaches prepares developers for diverse Angular codebases.
Sources
- NgRx v22 Release Announcement - Resource extensions, dynamic deep signals, SignalStoreFeatureType
- Angular 22 Feature Overview - Signal Forms stable, OnPush default, Resource API
- Using Angular's Resource API with NgRx Signal Store - withProps patterns, httpResource integration
Start practicing!
Test your knowledge with our interview simulators and technical tests.
Signal Store vs Classic NgRx: Key Takeaways
- Signal Store reduces state management boilerplate by 60-70% compared to classic NgRx
- NgRx 22 adds resource extensions (
withPreviousValueOnLoading,withValueOnError) for seamless async state handling - The
withPropsfeature centralizes service injection, eliminating repeated parameter lists across features watchStateenables synchronous state tracking for undo/redo and state history- Classic NgRx remains the better choice for enterprise applications requiring strict audit trails and complex side effects
- Both approaches coexist in the same application, enabling incremental migration
- For new Angular 22+ projects, Signal Store should be the default choice unless specific requirements demand classic NgRx
Can you spot the bug in Angular?
One real snippet, one hidden bug, one attempt a day. No account needed to try.

Written by
Anthony Fillion-MailletFounder of SharpSkill
Full-stack developer for over 10 years. Runs SharpSkill and answers for everything published here.
Updated on October 9, 2026
Tags
Share
Related articles

RxJS in Angular 2026: Operators, Subjects and Signals Interop
RxJS in Angular 2026: master the operators, Subjects, and Signals interop patterns Angular developers use in production, plus the interview questions that come up most.

Angular Signals and Computed in 2026: Fine-Grained Reactivity and Interview Questions
Master Angular Signals, computed, effect, and linkedSignal in Angular 20+. Learn fine-grained reactivity patterns, performance optimizations, and prepare for technical interviews.

Angular Resource API and httpResource: Complete Guide (Stable in Angular 22)
Angular's Resource API and httpResource are now stable as of Angular 22. This guide covers resource(), rxResource(), httpResource(), the new chain() helper for dependent resources, Zod validation, and common interview questions.