DEV Community

Cover image for Announcing NgRx Signals v18: State Encapsulation, Private Store Members, Enhanced Entity Management, and more!
Marko Stanimiroviฤ‡ for NgRx

Posted on

Announcing NgRx Signals v18: State Encapsulation, Private Store Members, Enhanced Entity Management, and more!

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 }); // โœ…
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 });
Enter fullscreen mode Exit fullscreen mode

๐Ÿ’ก 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 {},
  }))
);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

๐Ÿ’ก 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);
    },
  })
);
Enter fullscreen mode Exit fullscreen mode

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();
    },
  })
);
Enter fullscreen mode Exit fullscreen mode

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' }));
    },
  }))
);
Enter fullscreen mode Exit fullscreen mode

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 }));
    },
  }))
);
Enter fullscreen mode Exit fullscreen mode

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));
    },
  }))
);
Enter fullscreen mode Exit fullscreen mode

๐Ÿ’ก 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
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
jangelodev profile image
Joรฃo Angelo

Hi Marko Stanimiroviฤ‡,
Top, very nice and helpful !
Thanks for sharing.

Collapse
 
spock123 profile image
Lars Rye Jeppesen

Amazing

Collapse
 
shailesh_parshewar_d80220 profile image
Shailesh Parshewar

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

Collapse
 
rainerhahnekamp profile image
Rainer Hahnekamp • Edited

You were missing the classes, right? ๐Ÿ˜…

Collapse
 
shailesh_parshewar_d80220 profile image
Shailesh Parshewar

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 ๐Ÿ˜†

Collapse
 
yeongcheon profile image
YeongCheon

๐Ÿ‘

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