The NgRx team is thrilled to announce the release of NgRx version 20! This release aligns with the exciting launch of Angular v20 and represents a significant leap forward for state management in the Angular ecosystem. With NgRx v20, we are delivering on our promise to provide a modern, powerful, and developer-friendly set of tools. This version marks a pivotal moment of maturation for @ngrx/signals
, transforming it from a promising new library into a full-spectrum state management solution capable of tackling any challenge, from the simplest component state to the most complex enterprise-scale applications.
This release is built on three core themes:
- A Modern Take on Flux: We're introducing a powerful, optional, and experimental event-driven architecture with the new Events plugin, bringing the battle-tested patterns of Redux to the world of SignalStore.
- Unparalleled Developer Experience (DX): We've focused heavily on making your life easier with major improvements to testing, more powerful entity management, and enhanced tooling to ensure code quality.
-
Advanced Composition Patterns: We're unlocking new levels of sophistication and reusability with new tools like
withFeature
andwithLinkedState
, enabling you to build elegant and scalable state logic.
As always, this release would not be possible without our incredible community. Your feedback, contributions, and passion drive us forward. Thank you for being a part of this journey with us. Now, let's dive into what's new in NgRx v20!
A Modern Take on Flux: The Experimental Events Plugin π
One of the most significant additions in NgRx v20 is the new Events plugin for SignalStore, which is being released in an experimental stage. While the default method-driven approach of SignalStore is lightweight and perfect for many scenarios, we recognize that highly complex applications often benefit from a more decoupled, event-driven architecture.
Inspired by the original Flux architecture, the Events plugin provides an optional layer that brings robust, predictable patterns directly into the SignalStore ecosystem. By dispatching events and reacting to them, you decouple what happened from how the state should change, leading to more maintainable code, especially in scenarios with inter-store communication. This makes SignalStore a "full-spectrum" solution, capable of scaling from a simple service with signals to a complex, event-driven system.
For a deep dive into the plugin's architecture and a comprehensive walkthrough, check out our dedicated blog post:


Announcing Events Plugin for NgRx SignalStore: A Modern Take on Flux Architecture
Marko StanimiroviΔ for NgRx γ» May 12
Method/Command-driven vs. Event-driven SignalStore
To help you decide which pattern to use, hereβs a comparison of the "classic" method-driven SignalStore and the new event-driven approach with the Events plugin.
Feature | "Classic" SignalStore (Method/Command-driven) | SignalStore with Events Plugin (Event-driven) |
---|---|---|
Invocation | Component directly calls a method on the store instance. store.loadUsers()
|
Component dispatches an event. dispatcher.dispatch(usersPageEvents.opened())
|
Coupling | Tightly coupled. The component knows about the store and its specific methods. | Decoupled. The component only knows about the event, not which store(s) will handle it. |
State Change | Handled inside the method via patchState . |
Handled in a withReducer block, listening for specific events. |
Side Effects | Handled inside the method, often using rxMethod . |
Handled in a withEffects block, listening for specific events. |
Use Case | Ideal for local/feature state where the component and store are closely related. | Excels in complex scenarios with inter-store communication or when a fully decoupled architecture is desired. |
SignalStore Feature Enhancements
NgRx v20 is packed with new features that refine and extend the power of SignalStore, focusing on developer ergonomics and advanced composition.
Enhanced Entity Management: prependEntity
and upsertEntity
π§©
The @ngrx/signals/entities
plugin is already a powerful tool for managing collections of data. In v20, we've added two highly-requested updaters to give you even more fine-grained control.
First, we're excited to introduce prependEntity
, a community contribution from Dima Vasylyna. This function allows you to add an entity to the beginning of a collection, a common requirement for UIs like activity feeds, logs, or chat interfaces where the newest items appear at the top. If the entity collection has an entity with the same ID, it is not added and no error is thrown.
import { patchState } from '@ngrx/signals';
import { prependEntity } from '@ngrx/signals/entities';
// Adds the new todo to the start of the `entities` array
patchState(store, prependEntity({ id: 0, text: 'Newest item', completed: false }));
Second, we've added upsertEntity
. This powerful updater adds an entity if it doesn't exist or performs a merge update if it does. This is not a full replacement; it only merges the properties you provide, making it highly efficient for partial updates. This simplifies logic that would otherwise require you to first check for an entity's existence before deciding whether to add or update it.
import { patchState } from '@ngrx/signals';
import { upsertEntity } from '@ngrx/signals/entities';
// If entity with id: 1 exists, its `completed` prop is updated to true.
// If it does not exist, a new entity { id: 1, completed: true } is added to the collection.
patchState(store, upsertEntity({ id: 1, completed: true }));
These additions are a direct result of community feedback and reflect our focus on ergonomic refinement. By building these common patterns into the library, we reduce boilerplate, eliminate potential bugs in custom implementations, and make the entity management API more complete and intuitive.
Powerful Composition with withFeature
π οΈ
Custom store features, created with signalStoreFeature
, are fantastic for creating reusable, encapsulated logic. However, they have historically been self-contained. What if a reusable feature needs to access a method or property from the specific store it's being applied to?.
NgRx v20 introduces withFeature
as the elegant solution to this problem. It's a "feature factory" that receives the current store instance as an argument. This gives your inner feature full, type-safe access to all of the store's existing members, allowing you to create truly powerful and dynamic compositions.
Consider a generic withEntityLoader
feature. It defines the logic for loading an entity but delegates the actual data fetching to a loader
function provided by the store.
// A reusable feature that uses a loader function
function withEntityLoader<Entity>(loader: (id: string) => Observable<Entity>) {
return signalStoreFeature(
withState({ entity: undefined as Entity | undefined, isLoading: false }),
withMethods((store) => ({
loadEntity: rxMethod<string>(
pipe(
tap(() => patchState(store, { isLoading: true })),
// The loader function provided by the store is used here
switchMap((id) => loader(id).pipe(
tapResponse({
next: (entity) => patchState(store, { entity, isLoading: false }),
error: (error) => {
console.error(error);
patchState(store, { isLoading: false });
},
})
))
)
)
}))
);
}
// The store definition
const ProductsStore = signalStore(
withMethods((store, productsApi = inject(ProductsApi)) => ({
// Store-specific implementation of the load method
load(id: string): Observable<Product> {
return productsApi.fetchProduct(id);
},
})),
// `withFeature` connects the generic feature to the store's specific `load` method
withFeature(
(store) => withEntityLoader((id: string) => store.load(id))
)
);
Another example is a generic withBooksFilter
that can filter out an array of books and provides a method to set such filter. withFeature
is this case can be used to pass the specific array from the entities.
import { computed, Signal } from '@angular/core';
import {
patchState,
signalStore,
signalStoreFeature,
withComputed,
withFeature,
withMethods,
withState,
} from '@ngrx/signals';
import { withEntities } from '@ngrx/signals/entities';
import { Book } from './book';
export function withBooksFilter(books: Signal<Book[]>) {
return signalStoreFeature(
withState({ query: '' }),
withComputed(({ query }) => ({
filteredBooks: computed(() =>
books().filter((b) => b.name.includes(query()))
),
})),
withMethods((store) => ({
setQuery(query: string): void {
patchState(store, { query });
},
})),
)};
export const BooksStore = signalStore(
withEntities<Book>(),
// π Using `withFeature` to pass input to the `withBooksFilter` feature.
withFeature(({ entities }) => withBooksFilter(entities)),
);
The introduction of withFeature
elevates SignalStore from just a state management tool to a powerful framework for creating highly abstract and reusable state management libraries. It enables library authors and teams building large-scale applications to create generic features (e.g., withUndoRedo
, withPersistence
) that can be adapted to any store's specific context in a completely type-safe manner. This moves SignalStore beyond application-level state and into the realm of architectural pattern creation.
Introducing withLinkedState
for Advanced Reactivity π
NgRx v20 introduces withLinkedState
, a powerful feature for creating derived, reactive state that lives directly within the SignalStore. It allows you to define new state signals that are automatically computed whenever their source signals change.
In its simplest form, you can derive a new signal from existing state. For example, you can create a selectedOption
signal that always reflects the first item in an options
array:
const OptionsStore = signalStore(
withState({ options: [1, 2, 3] }),
withLinkedState(({ options }) => ({
selectedOption: () => options()[0],
}))
);
// The store now has a `selectedOption` signal of type `Signal<number | undefined>`.
// When the `options` signal changes, `selectedOption` automatically updates.
For more advanced scenarios, withLinkedState
can be combined with linkedSignal
(or any WritableSignal
) to create stateful computations. This is incredibly useful for patterns where you need to preserve a selection or state even when the underlying data source changes.
For instance, you can maintain the selectedOption
even if the options
array is reordered or updated, falling back to the first item only if the previously selected one is no longer available:
type Option = { id: number; label: string };
const OptionsStore = signalStore(
withState({ options: as Option }),
withLinkedState(({ options }) => ({
selectedOption: linkedSignal<Option, Option>({
source: options,
computation: (newOptions, previous) => {
// Try to find the previously selected option in the new list
const option = newOptions.find((o) => o.id === previous?.value.id);
// Fall back to the first option if not found
return option ?? newOptions;
},
})
}))
);
withLinkedState
provides a clean, declarative API for building complex, interconnected state within your store. It streamlines common reactive patterns, reduces boilerplate, and makes your state logic more explicit and maintainable.
Developer Experience and Tooling
A core focus of NgRx v20 is improving the day-to-day experience of our developers through better testing and tooling.
Simplified Testing with @ngrx/signals/testing
π§ͺ
We're happy to announce the new @ngrx/signals/testing
package, designed to make testing your SignalStores easier and more intuitive than ever.
Since v18, SignalStore state is encapsulated by default (protectedState: true
) to prevent accidental external mutations. This is a fantastic feature for production code, but it can make test setup verbose, as you would need to call a series of public methods to get the store into a specific state for a test.
To solve this, we're introducing the unprotected
helper. This is a testing-only utility that allows you to bypass state encapsulation and use patchState
directly on the store. This is perfect for the "Arrange" step of your tests, allowing you to set up a specific state scenario with minimal code.
Hereβs how it works:
// counter.store.spec.ts
import { TestBed } from '@angular/core/testing';
import { patchState } from '@ngrx/signals';
import { unprotected } from '@ngrx/signals/testing'; // π Import from testing plugin
import { CounterStore } from './counter.store';
describe('CounterStore', () => {
it('should have the correct double count after state is patched', () => {
const store = TestBed.inject(CounterStore);
// Use `unprotected` to bypass encapsulation for test setup
patchState(unprotected(store), { count: 10 });
// Assert against the computed signal
expect(store.doubleCount()).toBe(20);
});
});
This new helper reflects our pragmatic philosophy of balancing architectural purity with testing practicality. By providing a sanctioned "escape hatch" exclusively for testing (and placing it in a separate testing entry point to discourage production use), we acknowledge the real-world needs of developers and make the entire development lifecycle smoother.
Improved Code Quality with New ESLint Rule
To help you write more robust code, we've added a new rule to @ngrx/eslint-plugin
, thanks to a community contribution from wolfmanfx. This rule enforces explicit type invocation in certain generic functions.
This helps prevent a class of subtle bugs where TypeScript's type inference might be too lenient, especially in complex scenarios. By encouraging explicit type invocation, this rule ensures that you are consciously applying the correct types, leading to more robust, predictable, and maintainable code.
You can get this and other helpful rules by installing our ESLint plugin:
ng add @ngrx/eslint-plugin
.
Housekeeping and Future Direction
As the Angular and NgRx ecosystems evolve, we sometimes need to adjust our library to ensure it remains lean, focused, and aligned with the framework's direction.
@ngrx/component
Enters Maintenance Mode
With the release of NgRx v20, the @ngrx/component
package is officially entering maintenance mode. This means the package will only receive critical security and bug fixes moving forward, with no new features planned.
We are making this change because the primary problems that @ngrx/component
solved are now elegantly handled by the Angular framework itself.
- The
*ngrxLet
directive is now superseded by Angular's built-in@let
syntax, which provides a native solution for defining template variables. - The utility of the
*ngrxPush
pipe is greatly diminished with the rise of Signals and the move towards zoneless applications, which is now in developer preview with Angular v20.
We advise developers to migrate away from @ngrx/component
in new and existing applications, preferring the native Angular APIs instead. This decision reflects our commitment to a healthy ecosystem. A library that refuses to deprecate features that have been superseded by the core framework can become bloated and confusing. By making this change, we are sending a clear message: NgRx is here to extend Angular where necessary, not reinvent it. This builds trust and ensures that NgRx remains a focused and relevant part of your toolkit.
SignalStore popularity
We are also excited to announce that SignalStore
overtook ComponentStore
as the second most popular State Management library in Angular. This is an expected outcome as SignalStore
supersedes ComponentStore
.
The original NgRx Global Store still continues to hold the number one spot.
Getting Started with NgRx v20
Ready to get started? Upgrading is easy with the Angular CLI.
Run the following command to update your NgRx packages to version 20:
ng update @ngrx/store @ngrx/effects @ngrx/signals @ngrx/component @ngrx/entity @ngrx/router-store @ngrx/schematics @ngrx/eslint-plugin
To use NgRx v20, you'll need the following minimum versions:
- Angular v20.x
- Angular CLI v20.x
- TypeScript v5.8.x
Be sure to explore our updated documentation on ngrx.io for detailed guides and API references for all the new features!
A Big Thank You to Our Community! β€οΈ
NgRx is a community-driven project, and we are immensely grateful for everyone who contributes their time and expertise. Your bug reports, feature requests, documentation improvements, and pull requests are what make this project thrive.
We want to give a special shout-out to a few individuals for their direct contributions to this release:
-
Dima Vasylyna for implementing the
prependEntity
feature. - Murat Sari for the new ESLint rule to enforce type invocation.
We also want to extend a huge thank you to our sponsors. Your financial support is crucial for the continued development and maintenance of NgRx.
A special thanks to our Gold sponsor, Nx, and our Bronze sponsor, House of Angular.
Sponsor NgRx π€
If you are interested in sponsoring the continued development of NgRx, please visit our GitHub Sponsors page for different sponsorship options, or contact us directly to discuss other sponsorship opportunities.
Follow us on X / Twitter and LinkedIn for the latest updates about the NgRx platform.
Upcoming NgRx Workshops π
With NgRx usage continuing to grow with Angular, many developers and teams still need guidance on how to architect and build enterprise-grade Angular applications. We are excited to introduce upcoming workshops provided directly by the NgRx team!
We're offering one to three full-day workshops that cover the basics of NgRx to the most advanced topics. Whether your teams are just starting with NgRx or have been using it for a while - they are guaranteed to learn new concepts during these workshops.
Visit our workshops page for more details. The next workshops are scheduled for:
- September 24-26 (US Time) | Registration
- October 8-10 (EU Time) | Registration
What's Next?
We are incredibly excited about what you will build with NgRx v20. We encourage you to try out the new features and share your feedback with us. We are especially interested in hearing your thoughts on the new experimental Events plugin. Please open issues and discussions on our GitHub repository to let us know what you think.
To stay up-to-date with the latest news, follow us on Twitter and LinkedIn.
Thank you for being part of the NgRx community! Happy coding!
Top comments (4)
Great job on NgRx 20! π thanks for the continued work.
Quick question:
Is there any plan for an adapter or utility to bridge actions$ with withEffects()?
Or any recommended pattern for smooth integration and cohabitation with existing @ngrx/effects?
πππ
Nice shot π
Thx for the release.
Can we get more examples pls πππ