We are pleased to announce the latest major version of the NgRx framework with some exciting new features, bug fixes, and other updates.
Introducing NgRx Signals 🚦
Previously, we opened an RFC for a new state management solution that will provide first-class support for reactivity with Angular Signals. We're excited to introduce the @ngrx/signals
library.
The NgRx Signals library is built from the ground up with Angular Signals, opt-in RxJS interoperability, and includes entity management out of the box. Many thanks to Marko Stanimirović for proposing the new library, along with building and improving the library with feedback from the rest of the NgRx team, and the community. The Signals library starts a new generation of Angular applications built with NgRx and Angular Signals.
Getting Started
To install the @ngrx/signals
package, use your package manager of choice:
npm install @ngrx/signals
You can also use the ng add
command:
ng add @ngrx/signals@latest
Defining State
Not every piece of state needs its own store. For this use case, @ngrx/signals
comes with a signalState
utility function to quickly create and operate on small slices of state. This can be used directly in your component class, service, or a standalone function.
import { Component } from '@angular/core';
import { signalState, patchState } from '@ngrx/signals';
@Component({
selector: 'app-counter',
standalone: true,
template: `
Count: {{ state.count() }}
<button (click)="increment()">Increment</button>
<button (click)="decrement()">Decrement</button>
<button (click)="reset()">Reset</button>
`,
})
export class CounterComponent {
state = signalState({ count: 0 });
increment() {
patchState(this.state, (state) => ({ count: state.count + 1 }));
}
decrement() {
patchState(this.state, (state) => ({ count: state.count - 1 }));
}
reset() {
patchState(this.state, { count: 0 });
}
}
The patchState
utility function provides a type-safe way to perform immutable updates on pieces of state.
Creating a Store
For managing larger stores with more complex pieces of state, you can use the signalStore
utility function, along with patchState
, and other functions to manage the state.
import { computed } from '@angular/core';
import { signalStore, withState } from '@ngrx/signals';
export const CounterStore = signalStore(
withState({ count: 0 })
);
The withState
function takes the initial state of the store and defines the shape of the state.
Deriving Computed Values
Computed properties can also be derived from existing pieces of state in the store using the withComputed
function.
import { computed } from '@angular/core';
import { signalStore, patchState, withComputed } from '@ngrx/signals';
export const CounterStore = signalStore(
withState({ count: 0 }),
withComputed(({ count }) => ({
doubleCount: computed(() => count() * 2),
})),
);
The doubleCount
property is exposed as a property on the stored that reacts to changes to count
.
Defining Store Methods
You can also define methods that are exposed publicly to operate on the store with a well-defined API.
import { computed } from '@angular/core';
import { signalStore, patchState, withComputed, withMethods } from '@ngrx/signals';
export const CounterStore = signalStore(
withState({ count: 0 }),
withComputed(({ count }) => ({
doubleCount: computed(() => count() * 2),
})),
withMethods(({ count, ...store }) => ({
increment() {
patchState(store, { count: count() + 1 });
},
decrement() {
patchState(store, { count: count() - 1 });
},
}))
);
Defining Lifecycle Hooks
You can also create lifecycle hooks that are called when the store is created and destroyed, to initialize fetching data, updating state, and more.
import { computed } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { interval } from 'rxjs';
import {
signalStore,
withState,
patchState,
withComputed,
withHooks,
withMethods,
} from '@ngrx/signals';
export const CounterStore = signalStore(
withState({ count: 0 }),
withComputed(({ count }) => ({
doubleCount: computed(() => count() * 2),
})),
withMethods(({ count, ...store }) => ({
increment() {
patchState(store, { count: count() + 1 });
},
decrement() {
patchState(store, { count: count() - 1 });
},
})),
withHooks({
onInit({ increment }) {
interval(2_000)
.pipe(takeUntilDestroyed())
.subscribe(() => increment());
},
onDestroy({ count }) {
console.log('count on destroy', count());
},
}),
);
In the example above, the onInit
hook subscribes to an interval observable, calls the increment
method on the store to increment the count every 2 seconds. The lifecycle methods also have access to the injection context for automatic cleanup using takeUntilDestroyed()
.
Providing and Injecting the Store
To use the CounterStore
, add it to the providers
array of your component, and inject it using dependency injection.
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { CounterStore } from './counter.store';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<h1>Counter (signalStore)</h1>
<p>Count: {{ store.count() }}</p>
<p>Double Count: {{ store.doubleCount() }}</p>
<button (click)="store.increment()">Increment</button>
<button (click)="store.decrement()">Decrement</button>
`,
providers: [CounterStore],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class CounterComponent {
readonly store = inject(CounterStore);
}
Opt-in Interopability with RxJS
RxJS is still a major part of NgRx and the Angular ecosystem, and the NgRx Signals package provides opt-in usage to interact with RxJS observables using the rxMethod
function.
The rxMethod
function allows you to define a method on the signalStore that can receive a signal or observable, read its latest values, and perform additional operations with an observable.
import { inject } from '@angular/core';
import { debounceTime, distinctUntilChanged, pipe, switchMap, tap } from 'rxjs';
import {
signalStore,
patchState,
withHooks,
withMethods,
withState,
} from '@ngrx/signals';
import { rxMethod } from '@ngrx/signals/rxjs-interop';
import { tapResponse } from '@ngrx/operators';
import { User } from './user.model';
import { UsersService } from './users.service';
type State = { users: User[]; isLoading: boolean; query: string };
const initialState: State = {
users: [],
isLoading: false,
query: '',
};
export const UsersStore = signalStore(
{ providedIn: 'root' },
withState(initialState),
withMethods((store, usersService = inject(UsersService)) => ({
updateQuery(query: string) {
patchState(store, { query });
},
async loadAll() {
patchState(store, { isLoading: true });
const users = await usersService.getAll();
patchState(store, { users, isLoading: false });
},
loadByQuery: rxMethod<string>(
pipe(
debounceTime(300),
distinctUntilChanged(),
tap(() => patchState(store, { isLoading: true })),
switchMap((query) =>
usersService.getByQuery(query).pipe(
tapResponse({
next: (users) => patchState(store, { users }),
error: console.error,
finalize: () => patchState(store, { isLoading: false }),
}),
),
),
),
),
})),
withHooks({
onInit({ loadByQuery, query }) {
loadByQuery(query);
},
}),
);
The example UserStore
above uses the rxMethod
operator to create a method that loads the users on initialization of the store based on a query string.
The UsersStore
can then be used in the component, along with its additional methods, providing a clean, structured way to manage state with signals, combined with the power of RxJS observable streams for asynchronous behavior.
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { SearchBoxComponent } from './ui/search-box.component';
import { UserListComponent } from './ui/user-list.component';
import { UsersStore } from './users.store';
@Component({
selector: 'app-users',
standalone: true,
imports: [SearchBoxComponent, UserListComponent],
template: `
<h1>Users (RxJS Integration)</h1>
<app-search-box
[query]="store.query()"
(queryChange)="store.updateQuery($event)"
/>
<app-user-list [users]="store.users()" [isLoading]="store.isLoading()" />
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class UsersComponent {
readonly store = inject(UsersStore);
}
The @ngrx/signals
package also includes functionality for managing entities, composing shared features, sharing global stores, and can be extended to many different use cases.
Check out the NgRx docs to get more examples and usage on the new @ngrx/signals
packages.
The NgRx Signals package is in developer preview while we get feedback from usage and improve its APIs. We're also looking for some logo ideas for the NgRx Signals package! Check out the open issue and/or give us some suggestions!
Many thanks to Manfred Steyer and Rainer Hahnekamp from the Angular Architects team for their valuable feedback and presentations on the NgRx Signals library.
The Future of NgRx Libraries 🔮
With the introduction of the NgRx Signals library, you may be wondering what is going to happen with the other NgRx libraries.
The ecosystem around NgRx Store continues to scale very well for big enterprise applications, and we will continue to improve integration with Signals without disrupting its natural workflow.
NgRx ComponentStore was built to fill a gap of reactivity and structure around state management with RxJS at a local level before Angular Signals were introduced. It's also being used with enterprise apps and continues to provide value. Some people will stick with it longer as it's been around longer and is more battle-tested today. There are no plans to deprecate it.
NgRx Signals is a ground-up approach to managing state reactively, gets its roots from ComponentStore APIs, and is opt-in for RxJS usage. It also has other utilities for working with Angular Signals in a structured way that helps developers scale up. So it's not a replacement for ComponentStore in that way. We'll see as more people start to use NgRx Signals and make the experience better. It's inherited lots of what we've learned over maintaining these libraries for years.
Each package serves a purpose in the NgRx ecosystem and will continue to do so.
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!
Starting in January, we will offer 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.
The workshop covers both global state with NgRx Store and libraries, along with managing local state with NgRx ComponentStore and NgRx Signals.
Visit our workshops page to sign up from our list of upcoming workshops.
New NgRx Operators Package 🛠
As the NgRx framework packages have expanded, over time there have been some RxJS operators that are useful across many different areas. These operators are tapResponse
in the NgRx ComponentStore package, and concatLatestFrom
in the NgRx Effects package.
The tapResponse
operator provides an easy way to handle the response from an Observable in a safe way. It enforces that the error case is handled and that the effect would still be running should an error occur.
The concatLatestFrom
operator functions similarly to withLatestFrom
with one important difference - it lazily evaluates the provided Observable factory.
We are introducing the @ngrx/operators
package to provide these general-purpose operators to any Angular application. The NgRx ComponentStore and NgRx Effects packages still provide these operators as they are re-exported from the @ngrx/operators
package.
Better Performance with NgRx StoreDevtools
NgRx StoreDevtools provides a way to easily debug, trace, and inspect the flow of actions throughout your application when using NgRx Store. Previously, when connecting the StoreDevtools to the Redux Devtools Extension, this connection was done inside the context of Angular change detection with zone.js. This could lead to additional change detection cycles when interacting with the Redux Devtools.
We have improved this by connecting to the Redux Devtools outside the context of the Angular Zone, providing better performance for new and existing applications when using NgRx Store, and NgRx StoreDevtools together.
As part of the v17 upgrade, a migration is run as an opt-in for existing applications:
For module-based applications:
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
@NgModule({
imports: [
StoreDevtoolsModule.instrument({
connectInZone: true,
}),
],
bootstrap: [AppComponent],
})
export class AppModule {}
For applications using standalone APIs:
import { provideStoreDevtools } from '@ngrx/store-devtools';
bootstrapApplication(AppComponent, {
providers: [
provideStoreDevtools({
maxAge: 25,
logOnly: !isDevMode(),
connectInZone: true
}),
],
});
You can opt out of connecting in the context of Angular change detection with zone.js by removing the connectInZone
property from the StoreDevtools options. For new applications, this is the default behavior.
Thanks to Artur Androsovych for the contribution!
Dark Mode for the Docs 😎
Another long-awaited feature that has been added to our documentation is dark mode!
Many thanks to Mateusz Stefańczyk for the contribution!
NgRx Data in Maintenance Mode 👨🔧
Ward Bell and John Papa originally created the NgRx Data package as a way to use NgRx Store, Effects, and Entity to handle collections of data in a more structured way. While this package has provided some value to developers over the years, it has not continued to evolve as much as the rest of the NgRx Libraries to be more extensible.
With new libraries and patterns emerging, we have decided to move NgRx Data into maintenance mode.
This means:
- NgRx Data will not be deprecated in v17.
- New feature requests for this package won't be accepted.
- NgRx Data won't be recommended for new and existing projects.
We will recommend some strategies for migrating away from NgRx Data in the future for those looking to do so.
Deprecations and Breaking Changes 💥
This release contains bug fixes, deprecations, and breaking changes. For most of these deprecations or breaking changes, we've provided a migration that automatically runs when you upgrade your application to the latest version.
Take a look at the version 17 migration guide for complete information regarding migrating to the latest release. The complete CHANGELOG can be found in our GitHub repository.
Upgrading to NgRx 17 🗓️
To start using NgRx 17, make sure to have the following minimum versions installed:
- Angular version 17.x
- Angular CLI version 17.x
- TypeScript version 5.2.x
- RxJS version ^6.5.x or ^7.5.x
NgRx supports using the Angular CLI ng update
command to update your NgRx packages. To update your packages to the latest version, run the command:
ng update @ngrx/store
If your project uses @ngrx/component-store
, but not @ngrx/store
, run the following command:
ng update @ngrx/component-store
Swag Store and Discord Server 🦺
You can get official NgRx swag through our store! T-shirts with the NgRx logo are available in many different sizes, materials, and colors. We will look at adding new items to the store such as stickers, magnets, and more in the future. Visit our store to get your NgRx swag today!
Join our Discord server for those who want to engage with other members of the NgRx community, old and new.
Contributing to NgRx 🥰
We're always trying to improve the docs and keep them up-to-date for users of the NgRx framework. To help us, you can start contributing to NgRx. If you're unsure where to start, come take a look at our contribution guide and watch the introduction video Jan-Niklas Wortmann and Brandon Roberts have made to help you get started.
Thanks to all our contributors and sponsors!
NgRx continues to be a community-driven project. Design, development, documentation, and testing all are done with the help of the community. Visit our community contributors section to see every person who has contributed to the framework.
If you are interested in contributing, visit our GitHub page and look through our open issues, some marked specifically for new contributors. We also have active GitHub discussions for new features and enhancements.
We want to give a big thanks to our Gold sponsor, Nx! Nx has been a longtime promoter of NgRx as a tool for building Angular applications, and is committed to supporting open source projects that they rely on.
We also want to thank our Bronze sponsor, House of Angular!
Follow us on Twitter and LinkedIn for the latest updates about the NgRx platform.
Top comments (24)
Congratulations from my side, and thanks for mentioning Manfred and me.
We will continue to do our part to spread the word!
The new API-s look fancy, but I will definitely not go to re-write my stores or move away from the "old approach" of writing NgRx, for the same reason I never used the NgRx data in prod projects.
Anyway amazing work, it's nice to see that NgRx is up with the latest trends, I hope for many more successful versions.
I rewrote one of my stores today and it was an amazing experience.
That's great to hear. I remember your comment on my stream too. I'm going to give this a shot soon :)
Did you have a small or large-scale app that you migrated to the signal store?
Hey!!! Thanks for your great streams!
It was a new feature for an existing application so I made the SignalStore from scratch.
It's very simple to make, and quite less verbose than the ngrx store/effects.
So, you think that Google's engineers, the inventors of the V8 javascript machine and Node.js, are so light in javascript that they are unable to produce a framework Angular sufficient by itself to produce complex and heavy web apps, therefore you come to their rescue by superposing a second 2-way data binding between components (NgRx) to the native one of Angular. This is ridiculous.
Learn instead that Angular provides a native 2-way data binding between components by the means of standard services. React or Vues do not, and thus require a Redux like machinery, that Angular does not need. Angular and React are two different technologies, Angular is working with the change detection loop that does not exist in React. So stop coding Angular as if its 2-way data binding would not exist.
Thanks for your work. Is there a way to have signal store connected to NgRx devtools. I know it is argued that you can see the state with the Angular devtools but they are not comparable - NgRx devtools is way better.
1 Guys, as I understand it, selectors are represented by the withComputed hook. Is it possible to create a parameterized selector like this?
Or should it be done only at the component level?
2 Is it possible to "await" function from withMethods? Just to implement some logic after it finishes.
Congrats! That's awesome. Thanks for mentioning Rainer and my lesser self. We enjoyed using the new Signal Store from its very first days ❤️❤️❤️
As some one mentioned, there is ZERO mention of this pakage in te NGRX dos... as amazing as the package looks. Annoucing a brand new tools and linking to its non existant docs isnt a great look...
Hello,
I can't seem to find the docs for the signals package on ngrx.io.
Are they not up yet? You mentioned there are more examples there.
Congratulations!!!!! Definitely going to try it out soon and will share my experience :)
I just made my first component with a signalStore attached, and oh my god is it just nice!! fantastic!