Announcing NgRx v17: Introducing NgRx Signals, Operators, Performance Improvements, Workshops, and more!

brandontroberts

Brandon Roberts

Posted on November 22, 2023

Announcing NgRx v17: Introducing NgRx Signals, Operators, Performance Improvements, Workshops, and more!

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


Enter fullscreen mode Exit fullscreen mode

You can also use the ng add command:



ng add @ngrx/signals@latest


Enter fullscreen mode Exit fullscreen mode

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 });
  }
}


Enter fullscreen mode Exit fullscreen mode

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 })
);


Enter fullscreen mode Exit fullscreen mode

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),
  })),
);


Enter fullscreen mode Exit fullscreen mode

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 });
    },
  }))
);


Enter fullscreen mode Exit fullscreen mode

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());
    },
  }),
);


Enter fullscreen mode Exit fullscreen mode

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);
}


Enter fullscreen mode Exit fullscreen mode

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);
    },
  }),
);


Enter fullscreen mode Exit fullscreen mode

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);
}


Enter fullscreen mode Exit fullscreen mode

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 {}


Enter fullscreen mode Exit fullscreen mode

For applications using standalone APIs:



import { provideStoreDevtools } from '@ngrx/store-devtools';

bootstrapApplication(AppComponent, {
  providers: [
    provideStoreDevtools({
      maxAge: 25,
      logOnly: !isDevMode(),
      connectInZone: true
    }),
  ],
});


Enter fullscreen mode Exit fullscreen mode

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!

NgRx Docs 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


Enter fullscreen mode Exit fullscreen mode

If your project uses @ngrx/component-store, but not @ngrx/store, run the following command:



ng update @ngrx/component-store


Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
brandontroberts
Brandon Roberts

Posted on November 22, 2023

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

Sign up to receive the latest update from our blog.

Related