We are pleased to announce the latest major version of NgRx Signals, featuring exciting new features, bug fixes, and other updates.
NgRx Signals Are Now Stable! 🎉
As of version 18, the @ngrx/signals
package is out of developer preview and is now production-ready.
State Encapsulation 🔒
In previous versions, SignalStore's state could be updated from the outside.
// v17:
export const CounterStore = signalStore(
withState({ count: 0 }),
withMethods((store) => ({
setCount(count: number): void {
patchState(store, { count }); // ✅
},
}))
);
// ===
const store = inject(CounterStore);
patchState(store, { count: 10 }); // ✅
In version 18, the state is protected from external modifications by default, ensuring a consistent and predictable data flow.
// v18:
const CounterStore = signalStore(
withState({ count: 0 }),
withMethods((store) => ({
setCount(count: number): void {
patchState(store, { count }); // ✅
},
}))
);
// ===
const store = inject(CounterStore);
patchState(store, { count: 10 }); // ❌ compilation error
However, for more flexibility in some cases, external updates to the state can be enabled by setting the protectedState
option to false
when creating a SignalStore.
const CounterStore = signalStore(
{ protectedState: false }, // 👈
withState({ count: 0 })
);
// ===
const store = inject(CounterStore);
// ⚠️ The state is unprotected from external modifications.
patchState(store, { count: 10 });
💡 For backward compatibility, the migration schematic that adds
protectedState: false
to all existing signal stores is automatically executed on upgrade.
Ensuring Integrity of Store Members 👨⚕️
As of version 18, overriding SignalStore members is not allowed. If any store member is accidentally overridden, a warning will be displayed in development mode.
const CounterStore = signalStore(
withState({ count: 0 }),
withMethods(() => ({
// ⚠️ @ngrx/signals: SignalStore members cannot be overridden.
count(): void {},
}))
);
Private Store Members 🛡️
SignalStore allows defining private members that cannot be accessed from outside the store by using the _
prefix. This includes root-level state slices, computed signals, and methods.
const CounterStore = signalStore(
withState({
count1: 0,
// 👇 private state slice
_count2: 0,
}),
withComputed(({ count1, _count2 }) => ({
// 👇 private computed signal
_doubleCount1: computed(() => count1() * 2),
doubleCount2: computed(() => _count2() * 2),
})),
withMethods((store) => ({
increment1(): void { /* ... */ },
// 👇 private method
_increment2(): void { /* ... */ },
})),
);
// ===
const store = inject(CounterStore);
store.count1(); // ✅
store._count2(); // ❌ compilation error
store._doubleCount1(); // ❌ compilation error
store.doubleCount2(); // ✅
store.increment1(); // ✅
store._increment2(); // ❌ compilation error
💡 Learn more about private store members in the Private Store Members guide.
State Tracking 🕵
State tracking enables the implementation of custom SignalStore features such as logging, state undo/redo, and storage synchronization.
In previous versions, it was possible to track SignalStore state changes by using the getState
function with effect
.
const CounterStore = signalStore(
withState({ count: 0 }),
withMethods((store) => ({
increment(): void { /* ... */ },
})),
withHooks({
onInit(store) {
effect(() => {
// 👇 The effect is re-executed on state change.
const state = getState(store);
console.log('counter state', state);
});
setInterval(() => store.increment(), 1_000);
},
})
);
Due to the effect glitch-free behavior, if the state is changed multiple times in the same tick, the effect function will be executed only once with the final state value. While the asynchronous effect execution is beneficial for performance reasons, functionalities such as state undo/redo require tracking all SignalStore's state changes without coalescing state updates in the same tick.
In this release, the @ngrx/signals
package provides the watchState
function, which allows for synchronous tracking of SignalStore's state changes. It accepts a SignalStore instance as the first argument and a watcher function as the second argument.
By default, the watchState
function needs to be executed within an injection context. It is tied to its lifecycle and is automatically cleaned up when the injector is destroyed.
const CounterStore = signalStore(
withState({ count: 0 }),
withMethods((store) => ({
increment(): void { /* ... */ },
})),
withHooks({
onInit(store) {
watchState(store, (state) => {
console.log('[watchState] counter state', state);
}); // logs: { count: 0 }, { count: 1 }, { count: 2 }
effect(() => {
console.log('[effect] counter state', getState(store));
}); // logs: { count: 2 }
store.increment();
store.increment();
},
})
);
In the example above, the watchState
function will execute the provided watcher 3 times: once with the initial counter state value and two times after each increment. Conversely, the effect
function will be executed only once with the final counter state value.
💡 Learn more about the
watchState
function in the State Tracking guide.
Enhanced Entity Management 🧩
In version 18, the @ngrx/signals/entities
plugin received several enhancements.
selectId
In previous versions, updating an entity collection with a custom identifier was performed using the idKey
property.
// v17:
type Todo = { _id: number; text: string };
const TodosStore = signalStore(
withEntities<Todo>(),
withMethods((store) => ({
addTodo(todo: Todo): void {
patchState(store, addEntity(todo, { idKey: '_id' }));
},
}))
);
If an entity has a composite identifier (a combination of two or more properties), idKey
cannot be used.
In this release, selectId
is introduced instead of idKey
for better flexibility.
// v18:
type Todo = { _id: number; text: string };
const selectId: SelectEntityId<Todo> = (todo) => todo._id;
const TodosStore = signalStore(
withEntities<Todo>(),
withMethods((store) => ({
addTodo(todo: Todo): void {
patchState(store, addEntity(todo, { selectId }));
},
}))
);
entityConfig
The entityConfig
function reduces repetitive code when defining a custom entity configuration and ensures strong typing. It accepts a config object where the entity type is required, and the collection name and custom ID selector are optional.
const todoConfig = entityConfig({
entity: type<Todo>(),
collection: 'todo',
selectId: (todo) => todo._id,
});
const TodosStore = signalStore(
withEntities(todoConfig),
withMethods((store) => ({
addTodo(todo: Todo): void {
patchState(store, addEntity(todo, todoConfig));
},
}))
);
💡 Learn more about the
@ngrx/signals/entities
plugin in the Entity Management guide.
Upgrading to NgRx Signals 18 🛠️
To start using NgRx Signals 18, make sure to have the following minimum versions installed:
- Angular version 18.x
- Angular CLI version 18.x
- TypeScript version 5.4.x
NgRx supports using the Angular CLI ng update
command to update your NgRx packages. To update the @ngrx/signals
package to the latest version, run the command:
ng update @ngrx/signals@18
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 to sign up from our list of upcoming workshops. The next workshops are scheduled for September 18-20 (US-based time) and October 16-18 (Europe-based time). The early bird discount is still active!
Welcoming New Additions to the NgRx Team 💪
After a long break, Mike Ryan is back on the NgRx core team! As one of the original co-creators of NgRx, Mike wrote the first lines of code for the @ngrx/effects
, @ngrx/entity
, and @ngrx/store-devtools
packages. We are thrilled to have him return to the team! Welcome back, Mike! 💜
We have another exciting announcement: Rainer Hahnekamp has joined the NgRx team as a trusted collaborator! Rainer is a Google Developer Expert in Angular, an active contributor to the NgRx repository, a maintainer of the NgRx Toolkit community plugin, and the creator of numerous insightful articles and videos on NgRx. Welcome aboard, Rainer! 🚀
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 want to thank our Bronze sponsor, House of Angular!
Lastly, we also want to thank our individual sponsors who have donated once or monthly.
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 Twitter and LinkedIn for the latest updates about the NgRx platform.
Top comments (6)
Hi Marko Stanimirović,
Top, very nice and helpful !
Thanks for sharing.
Amazing
Never heard of ngRx but could still understand code.. Ngl For the whole time I thought I could use it with react in some way 😅. Great job
You were missing the classes, right? 😅
Well my college still teaches php so I am on my own in learning web dev 😅.. Since I have finished college this year so I am free to learn new things.. Hopefully it won't be as hard as learning about JSON 😆
👍
Some comments may only be visible to logged-in visitors. Sign in to view all comments. Some comments have been hidden by the post's author - find out more