State Management in Angular: NgRx vs. SignalStore

Hero image for State Management in Angular: NgRx vs. SignalStore. Image by Nick Fewings.
Hero image for 'State Management in Angular: NgRx vs. SignalStore.' Image by Nick Fewings.

State management in Angular used to be a much narrower conversation. If an application outgrew local component state, the answer often became some flavour of NgRx Store. That is still a strong option, but it is no longer the only one inside the NgRx ecosystem. SignalStore changes the discussion because it lowers the ceremony around feature state and leans into Angular's signal model directly.

That does not mean NgRx Store is obsolete, and it does not mean SignalStore is automatically better because it is newer. The more useful question is what kind of state we are dealing with, how widely it needs to be shared, how much event history matters, and how much architecture the team genuinely wants around it.

This is also why the comparison is slightly misleading. SignalStore is not antiNgRx. It is another NgRx tool. The real decision is less "old versus new" and more "global eventdriven store versus signalbased feature store".


The Misleading Part of the Comparison

NgRx Store and SignalStore do not optimise for exactly the same thing.

Classic NgRx Store is strongest when we want:

  • one clear applicationwide state model
  • explicit actions flowing through reducers
  • effects coordinating async work and side effects
  • selectors describing how state is derived
  • strong devtools support and a visible event history

SignalStore is strongest when we want:

  • state close to the feature that owns it
  • direct signal reads in Angular templates or component logic
  • less boilerplate around ordinary updates
  • a more local, serviceshaped state container
  • a simpler path for teams that do not need reducer ceremony everywhere

That difference matters because a lot of teams argue about API style when the real question is scope and workflow.


What Classic NgRx Store is Optimised for

NgRx Store is built around explicit state transitions. State changes happen because actions are dispatched. Reducers decide how the state changes. Selectors read derived slices. Effects deal with async work or other side effects that should sit outside the reducer.

That structure can feel heavy on a small feature, but it becomes useful when the application is large enough that state changes need to be predictable, inspectable, and shareable across several areas.

Here is a trimmed example of that shape:

import {  createActionGroup,  createFeature,  createReducer,  emptyProps,  on,  props,} from '@ngrx/store';type Product = {  id: number;  title: string;};type ProductsState = {  query: string;  loading: boolean;  items: Product[];};const initialState: ProductsState = {  query: '',  loading: false,  items: [],};export const ProductsActions = createActionGroup({  source: 'Products',  events: {    'Set Query': props<{ query: string }>(),    Load: emptyProps(),    'Load Success': props<{ items: Product[] }>(),  },});export const productsFeature = createFeature({  name: 'products',  reducer: createReducer(    initialState,    on(ProductsActions.setQuery, (state, { query }) => ({      ...state,      query,    })),    on(ProductsActions.load, (state) => ({      ...state,      loading: true,    })),    on(ProductsActions.loadSuccess, (state, { items }) => ({      ...state,      loading: false,      items,    }))  ),});

This gives us a very explicit contract. That is the whole appeal. A large team can usually answer questions like "what changed this state?" or "where did this load begin?" more easily when the answer lives in actions and effects instead of inside several ad hoc services.


What SignalStore is Optimised for

SignalStore reduces that ceremony by treating state more like a structured signaldriven service. We define state, computed values, and methods in one place, then consume that store directly through Angular's signal model.

That often feels better when the state is featurescoped rather than applicationwide.

import { computed } from '@angular/core';import {  patchState,  signalStore,  withComputed,  withMethods,  withState,} from '@ngrx/signals';type Product = {  id: number;  title: string;};type ProductsState = {  query: string;  loading: boolean;  items: Product[];};export const ProductsStore = signalStore(  withState<ProductsState>({    query: '',    loading: false,    items: [],  }),  withComputed(({ query, items }) => ({    filteredItems: computed(() => {      const currentQuery = query().trim().toLowerCase();      return items().filter((item) =>        item.title.toLowerCase().includes(currentQuery)      );    }),  })),  withMethods((store) => ({    setQuery(query: string): void {      patchState(store, { query });    },    loadSuccess(items: Product[]): void {      patchState(store, { items, loading: false });    },  })));

The important point is not that the code is shorter. The important point is that the state model is closer to the feature, direct to read, and easier to evolve when we do not need the full actionreducereffects workflow.


Where SignalStore Usually Feels Better

SignalStore is often the better fit when the state belongs to one route, one feature area, or one bounded piece of UI behaviour.

That might include:

  • filter state for a search page
  • editable form state that several nested components share
  • featurelevel loading and error state
  • derived state that wants to live close to the template
  • routescoped state that should disappear cleanly when the feature is left

This is where SignalStore tends to feel like a cleaner Angularnative experience. Signals are read directly. Computed values are explicit. Methods update state without the ceremony of creating actions just to move a small piece of local feature state from one value to another.

It also tends to cooperate more naturally with TypeScript because the store shape is more direct. We are not constantly threading types through actions, reducer cases, selectors, and effect streams for concerns that may not need that much infrastructure.


Where Classic NgRx Store Still Wins

NgRx Store still has real advantages, and teams sometimes flatten those too quickly.

It remains a stronger fit when:

  • state is genuinely global and shared across several distant parts of the app
  • we care about a visible event log of what happened and in what order
  • effects are coordinating several async workflows
  • the team benefits from strict architectural conventions around updates
  • Redux DevTools and action history are part of daily debugging

This is especially true in applications with lots of crosscutting behaviour. If one action should update several slices, trigger analytics, coordinate caching, and influence behaviour elsewhere, the explicit event model of NgRx Store is often an asset rather than overhead.

That is the part SignalStore does not replace. SignalStore can hold shared state, but it does not automatically give us the same eventfirst architecture and global traceability.


The Real Choice is Often Scope, Not Preference

A lot of Angular state discussions become cleaner if we ask the scope question first:

  • componentlocal state: plain Angular signals may be enough
  • featureshared state: SignalStore is often a strong fit
  • appwide, eventheavy state: NgRx Store may justify its structure

That is also why "NgRx or SignalStore?" is sometimes the wrong starting point. The better question can be "does this state even deserve a store?"

One of the quieter mistakes in Angular codebases is moving ordinary UI state into a global store too early. If a search panel, wizard, or feature page owns the state entirely, forcing it through global reducers and actions can make the design heavier than the problem needs.


Migration is Not All or Nothing

This is another area where teams overcorrect.

If an application already uses classic NgRx Store widely, there is rarely a strong reason to replace everything with SignalStore in one sweep. That is usually a large rewrite with unclear payoff.

A more sensible path is coexistence:

  • keep NgRx Store where the applicationlevel event model is already useful
  • use SignalStore for new feature state that does not need global reducer architecture
  • let the boundaries become clearer through real usage rather than through ideology

That kind of mixed model is often more honest. Large applications rarely have only one kind of state problem.


SignalStore Does Not Make RxJS Disappear

It is worth being clear about this. SignalStore works well with Angular signals, but Angular applications still live in a world of HTTP requests, router events, forms, and other async streams.

So the real decision is not "signals or RxJS?" It is usually:

  • signals for direct state reads, derived values, and local feature modelling
  • RxJS where streaming, cancellation, or event composition genuinely matters

That is one reason I would resist selling SignalStore as a universal replacement for all previous NgRx patterns. It is a better fit for some state shapes, not a magic eraser for every asynchronous concern in the application.


Common Mistakes

Using NgRx Store for Every Small UI Concern

If every filter toggle or transient panel state becomes a global action flow, the store layer can turn into ceremony rather than clarity.

Using SignalStore for Globally Important Event Flows Just Because It is Newer

If the real requirement is traceable, crosscutting application state with explicit events, the classic store may still be the better tool.

Forgetting That Plain Angular Signals May Be Enough

Not every feature needs any NgRx abstraction at all. Sometimes a component or a small injected service is already the right boundary.


Useful References

For the API details behind these patterns, the official documentation is still the strongest place to start:


Wrapping Up

NgRx Store and SignalStore are both strong tools, but they solve slightly different state management problems. NgRx Store earns its ceremony when the application needs explicit events, reducers, effects, and global traceability. SignalStore earns its place when feature state wants to stay closer to the UI, with less boilerplate and more direct signalbased ergonomics.

Key Takeaways

  • SignalStore is not a rejection of NgRx. It is a different NgRx abstraction with a different sweet spot.
  • Classic NgRx Store is strongest for global, eventdriven, crosscutting state.
  • SignalStore is often stronger for featurescoped shared state with lower ceremony.
  • The best first question is usually about scope, not about which API looks more modern.

Once we frame the decision around the shape of the state rather than around fashion, the NgRx versus SignalStore discussion becomes much easier to navigate.


Planning a platform change?

I help teams make difficult platform work clearer, from architecture decisions and migrations to launch recovery, performance, and search visibility.