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

markostanimirovic

Marko Stanimirović

Posted on July 25, 2024

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.

💖 💪 🙅 🚩
markostanimirovic
Marko Stanimirović

Posted on July 25, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related