Announcing NgRx v17: Introducing NgRx Signals, Operators, Performance Improvements, Workshops, and more!
Tags: angular, ngrx
Published: 2023-11-22 16:44:42

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](https://github.com/ngrx/platform/discussions/3796) 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ć](https://twitter.com/markostdev) 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:

```sh
npm install @ngrx/signals
```

You can also use the `ng add` command:

```sh
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.

```ts
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.

```ts
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.

```ts
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.

```ts
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.

```ts
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.

```ts
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.

```ts
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.

```ts
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](https://ngrx.io) 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](https://github.com/ngrx/platform/issues/4120) and/or give us some suggestions!

Many thanks to [Manfred Steyer](https://twitter.com/manfredsteyer) and [Rainer Hahnekamp](https://twitter.com/rainerhahnekamp) 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](https://ngrx.io/workshops) 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:

```ts
import { StoreDevtoolsModule } from '@ngrx/store-devtools';

@NgModule({
  imports: [
    StoreDevtoolsModule.instrument({
      connectInZone: true,
    }),
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}
```

For applications using standalone APIs:

```ts
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](https://github.com/arturovt) 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7z4ie40h8yxscawyna7a.png)

Many thanks to [Mateusz Stefańczyk](https://twitter.com/m_stefanczyk) 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](https://ngrx.io/guide/migration/v17) for complete information regarding migrating to the latest release. The complete [CHANGELOG](https://github.com/ngrx/platform/blob/master/CHANGELOG.md) 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:

```sh
ng update @ngrx/store
```

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

```sh
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](https://ngrx.threadless.com) to get your NgRx swag today!

Join our [Discord server](https://discord.com/invite/ngrx) 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](https://ngrx.io/contributing) and watch the [introduction video](https://youtu.be/ug0c1tUegm4) [Jan-Niklas Wortmann](https://twitter.com/niklas_wortmann) and [Brandon Roberts](https://twitter.com/brandontroberts) 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](https://ngrx.io/about?group=Community) to see every person who has contributed to the framework.

If you are interested in contributing, visit our [GitHub](https://github.com/ngrx/platform) 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](https://nx.app/company)! 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](https://houseofangular.io)!


Follow us on [Twitter](https://twitter.com/ngrx_io) and [LinkedIn](https://www.linkedin.com/company/ngrx) for the latest updates about the NgRx platform.