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 vs Classic NgRx Angular state management comparison

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.

Quick Decision Guide

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.

AspectClassic NgRxSignal Store
Mental ModelRedux/FluxReactive Services
BoilerplateHigh (actions, reducers, selectors, effects)Low (state, methods, computed)
ReactivityRxJS ObservablesAngular Signals
Resource APIManual integrationNative with @ngrx/signals/resource
DevToolsFull Redux DevToolsSignal Store DevTools (via plugin)
Learning CurveSteepModerate
Bundle SizeLarger (~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.

books.actions.tstypescript
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.

books.reducer.tstypescript
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.

books.selectors.tstypescript
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."

books.store.tstypescript
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.

books-resource.store.tstypescript
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.

books-entity.store.tstypescript
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.

with-request-status.tstypescript
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.

books-with-status.store.tstypescript
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.

state-history.tstypescript
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.

books-list.component.tstypescript
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 Requirements

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

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 withProps feature centralizes service injection, eliminating repeated parameter lists across features
  • watchState enables 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
Daily challenge

Can you spot the bug in Angular?

One real snippet, one hidden bug, one attempt a day. No account needed to try.

Anthony Fillion-Maillet

Written by

Anthony Fillion-Maillet

Founder of SharpSkill

Full-stack developer for over 10 years. Runs SharpSkill and answers for everything published here.

Updated on October 9, 2026

Tags

#angular
#ngrx
#state-management
#signals
#typescript

Share

Related articles