NgRx: To Be or Not to Be in Modern Angular
Evaluate whether NgRx still makes sense in modern Angular applications with signals, computed state, and resource APIs. Pros, cons, and a practical decision framework.
NgRx has been the default state management library for enterprise Angular applications for years. But Angular now ships with signals, computed values, effects, linkedSignal, and resource — covering much of what NgRx was originally needed for. The question is no longer “should I use NgRx?” but “does NgRx still solve a problem that Angular itself does not?”
This article breaks down what NgRx gives you, what Angular now handles natively, and how to decide whether NgRx belongs in your architecture.
NgRx current version (2026)
As of early 2026, the latest stable release is NgRx 21.0.1 (December 2025), aligned with Angular 19. The key packages are:
@ngrx/store21.0.1 — the core global store@ngrx/signals21.0.1 — the newer SignalStore API (signalStore,signalState,withEntities,withMethods,withComputed)@ngrx/effects21.0.1 — side effect management@ngrx/entity21.0.1 — normalized entity collections
NgRx 21 continues to support both the classic store pattern and the newer SignalStore API. The team has made it clear that both approaches will be maintained — the classic store for existing codebases and SignalStore for new projects that want signals-first patterns with more structure than a plain service.
What NgRx gives you today
NgRx is a reactive state management framework built on RxJS. It provides a structured pattern for managing global state using actions, reducers, selectors, and effects. The key capabilities include:
- Centralized store with a single source of truth for application-wide state
- DevTools integration for time-travel debugging and action inspection
- Effects for managing side effects (HTTP calls, navigation, logging) as reactive streams
- Entity adapter for normalized collection management with CRUD helpers
- Router store for synchronizing Angular Router state into the store
- Component store for localized, feature-scoped state management
- Strict unidirectional data flow enforced by the action-reducer pattern
For large teams, NgRx enforces consistency. Every developer follows the same pattern: dispatch an action, reduce state, select derived values. Code reviews become predictable and architectural drift is easier to spot.
What Angular provides natively now
Angular 17+ introduced signals as a core reactive primitive, and subsequent versions have expanded the API significantly:
signal()for writable reactive statecomputed()for derived state that updates automaticallyeffect()for side effects that react to signal changeslinkedSignal()for two-way binding patterns and dependent state resetresource()andrxResource()for declarative async data fetchinginput()andmodel()as signal-based component APIstoSignal()/toObservable()for interop with RxJS
These APIs cover the most common use cases that previously required NgRx: local state, derived state, fetching data, and reacting to changes. For feature-scoped state, a signals-based service often replaces an entire NgRx feature module.
When NgRx still makes sense
NgRx is not obsolete. There are specific architectural requirements where it provides value that signals alone do not:
Complex cross-feature state coordination
When multiple features need to react to the same state changes and the coordination logic is non-trivial, NgRx effects and selectors provide a proven pattern. Signals work well within a feature boundary, but cross-feature orchestration with cancellation, retry, and sequencing is where RxJS-based effects have an advantage.
Audit and debugging requirements
If your team needs to inspect every state transition, replay actions, or debug production issues by reviewing action logs, NgRx DevTools are unmatched. There is no signals equivalent for time-travel debugging or action history inspection.
Teams with established NgRx expertise
A large codebase with 50+ NgRx feature states, trained developers, and working patterns should not be rewritten for the sake of being “modern.” The migration cost often exceeds the benefit, especially when the existing architecture is stable and well-tested.
Normalized entity management at scale
If your application manages large normalized collections (thousands of items with relationships), NgRx Entity provides adapter utilities that reduce boilerplate for add, update, remove, and query operations. Rebuilding this with signals is possible but requires custom code.
When NgRx is overkill
For many applications, NgRx introduces unnecessary complexity without a proportional benefit:
Feature-scoped state
If state belongs to a single route or feature and does not need to be shared globally, a signals-based service is simpler, faster to write, and easier to test:
@Injectable()
export class ProductListStore {
private readonly _products = signal<Product[]>([]);
private readonly _filter = signal('');
readonly products = computed(() =>
this._products().filter(p =>
p.name.toLowerCase().includes(this._filter().toLowerCase())
)
);
readonly count = computed(() => this.products().length);
setFilter(value: string) { this._filter.set(value); }
loadProducts() {
// Use resource() or inject HttpClient
}
}
Compare this to the NgRx equivalent, which requires actions, a reducer, selectors, and an effect — four files and significantly more code for the same result.
Simple CRUD applications
Applications that primarily fetch, display, and update data through forms rarely benefit from centralized state. The data lifecycle is short (fetch → display → submit), and component-local state or a lightweight service handles it well.
Small teams
NgRx patterns are designed to enforce consistency across large teams. A team of two to five developers communicating directly does not need the ceremony of actions and reducers to stay aligned. The overhead slows development without adding proportional safety.
Practical comparison: product list feature
With NgRx
// actions
export const loadProducts = createAction('[Products] Load');
export const loadProductsSuccess = createAction(
'[Products] Load Success',
props<{ products: Product[] }>()
);
// reducer
export const productsReducer = createReducer(
initialState,
on(loadProductsSuccess, (state, { products }) => ({
...state,
products,
loaded: true
}))
);
// effect
loadProducts$ = createEffect(() =>
this.actions$.pipe(
ofType(loadProducts),
switchMap(() =>
this.http.get<Product[]>('/api/products').pipe(
map(products => loadProductsSuccess({ products }))
)
)
)
);
// selector
export const selectProducts = createSelector(
selectProductsState,
state => state.products
);
With signals
@Injectable()
export class ProductStore {
private readonly http = inject(HttpClient);
private readonly _products = signal<Product[]>([]);
readonly products = this._products.asReadonly();
readonly loaded = computed(() => this._products().length > 0);
loadProducts() {
this.http.get<Product[]>('/api/products').subscribe(
products => this._products.set(products)
);
}
}
The signals version achieves the same result with significantly less code, no external dependencies, and no global state registration. For features that do not need cross-boundary coordination, this reduction in complexity is a clear win.
Migration path: reducing NgRx surface gradually
If your application already uses NgRx and you want to reduce its footprint, avoid a full rewrite. Instead, migrate incrementally:
Step 1: Stop adding new NgRx feature states
For any new feature, default to a signals-based service. Only introduce NgRx if the feature genuinely requires cross-feature coordination or DevTools debugging.
Step 2: Migrate component store usages first
NgRx Component Store was already a step toward feature-scoped state. These are the easiest to convert to signal-based services because they are already isolated from the global store.
Step 3: Identify low-traffic global state slices
Find store slices that are only consumed by one feature. These can be moved into a feature-level signal service without affecting other parts of the application.
Step 4: Keep stable, cross-cutting state in NgRx
Authentication, user preferences, permissions, and other cross-cutting concerns that benefit from DevTools and action logging should remain in NgRx until signals-based tooling catches up.
Step 5: Re-evaluate every six months
Angular is actively expanding the signals API. Features like resource() and linkedSignal() reduce the gap further. Re-assess your NgRx footprint regularly.
Decision checklist
Use this to evaluate whether NgRx belongs in your project:
- Does the feature state need to be shared across multiple lazy-loaded routes? → Consider NgRx
- Do you need time-travel debugging or action logging for production issues? → NgRx DevTools
- Is the team larger than 8-10 developers working on the same codebase? → NgRx patterns help enforce consistency
- Is the state feature-scoped and only consumed by one route? → Signals service
- Is the application primarily CRUD with short data lifecycles? → Signals service
- Are you starting a new project with Angular 17+? → Default to signals, add NgRx only if needed
- Does the feature require complex async orchestration (retries, polling, cancellation)? → NgRx effects or RxJS directly
FAQ
Is NgRx dead?
No. NgRx is actively maintained and has adapted to support signals through signalStore and signalState. It remains a valid choice for applications with complex state coordination needs. What has changed is that Angular’s native APIs now cover the majority of use cases that previously required NgRx.
Should I remove NgRx from an existing application?
Not unless it is causing measurable problems. If your NgRx architecture is stable, well-tested, and understood by the team, the migration cost is rarely justified. Instead, stop expanding the NgRx surface for new features and migrate individual slices opportunistically.
Can NgRx and signals coexist?
Yes. NgRx already provides interop utilities for signals. You can use toSignal() on NgRx selectors and consume store values in signal-based components. A hybrid approach is practical and does not require an all-or-nothing decision.
What about NgRx SignalStore?
NgRx SignalStore is the team’s response to Angular signals. It provides a structured pattern for signals-based state with features like withEntities, withMethods, and withComputed. It is a middle ground: more structure than a plain signal service, less ceremony than the classic store. Consider it if you want opinionated patterns without full NgRx.
When will signals fully replace NgRx?
Signals will not fully replace NgRx because they solve different problems. Signals handle synchronous reactive state and derived values. NgRx handles async orchestration, cross-feature coordination, and debugging tooling. The overlap is growing, but the tooling and patterns gap for complex use cases has not closed yet.
Conclusion and next steps
NgRx is not a universal requirement for Angular applications anymore. For feature-scoped state, simple CRUD, and small-to-medium teams, Angular signals provide a lighter, faster, and more maintainable alternative. NgRx still earns its place in large codebases with complex coordination, strict debugging requirements, or established team patterns.
Start by evaluating your next new feature: can it work with a signals-based service? If yes, build it that way and compare the developer experience. For a deeper look at how signals fit into enterprise state architecture, read Angular Signals for Enterprise State Architecture. If you want to understand the boundary between signals and RxJS at the reactive-primitive level, see Angular Signals vs RxJS.
Modern Angular Architecture