Announcing NgRx v16: Integration with Angular Signals, Functional Effects, Standalone Schematics, and more!
Tags: ngrx, angular, webdev, opensource
Published: 2023-05-09 16:40:57

We are pleased to announce the latest major version of the NgRx framework with some exciting new features, bug fixes, and other updates.

---

## NgRx 🤝 Signals

As part of the v16 release, Angular introduced a new reactivity model powered by signals. Signal APIs provide a simple way of managing state and will significantly improve the rendering performance of Angular applications in the future.

In v16, NgRx Store has a new method, `selectSignal`, which provides integration with Angular Signals. This method expects a selector as an input argument and returns a signal of the selected state slice. It has a similar signature to the `select` method, but unlike `select`, `selectSignal` returns a signal instead of an observable.

```ts
import { Component, inject } from '@angular/core';
import { NgFor } from '@angular/common';
import { Store } from '@ngrx/store';

import { selectUsers } from './users.selectors';

@Component({
  standalone: true,
  imports: [NgFor],
  template: `
    <h1>Users</h1>

    <ul>
      <li *ngFor="let user of users()">
        {{ user.name }}
      </li>
    </ul>
  `
})
export class UsersComponent {
  private readonly store = inject(Store);

  // type: Signal<User[]>
  readonly users = this.store.selectSignal(selectUsers);
}
```

ComponentStore also provides the `selectSignal` method, which has two signatures. The first signature creates a signal from the provided state projector function, while the second creates a signal by combining provided signals, similar to the `select` method that combines provided observables.

```ts
import { Injectable } from '@angular/core';
import { ComponentStore } from '@ngrx/component-store';

import { User } from './user.model';

type UsersState = { users: User[]; query: string };

@Injectable()
export class UsersStore extends ComponentStore<UsersState> {
  // type: Signal<User[]>
  readonly users = this.selectSignal((s) => s.users);
  // type: Signal<string>
  readonly query = this.selectSignal((s) => s.query);
  // type: Signal<User[]>
  readonly filteredUsers = this.selectSignal(
    this.users,
    this.query,
    (users, query) =>
      users.filter(({ name }) => name.includes(query))
  );
}
```

Similar to the `computed` function, the `selectSignal` method also accepts the equality function to stop the recomputation of the deeper dependency chain if two values are determined to be equal.

The `state` signal is another addition to the ComponentStore. Instead of using `selectSignal`, it can be used together with the `computed` function to create derived signals.

```ts
import { computed, Injectable } from '@angular/core';
import { ComponentStore } from '@ngrx/component-store';

import { User } from './user.model';

type UsersState = { users: User[]; query: string };

@Injectable()
export class UsersStore extends ComponentStore<UsersState> {
  readonly users = computed(() => this.state().users);
  readonly query = computed(() => this.state().query);

  readonly filteredUsers = computed(() =>
    this.users().filter(({ name }) => name.includes(this.query()))
  );
}
```

---

## Functional Effects 🔥

In the past, we couldn't create NgRx effects outside of classes because Angular only supported constructor-based dependency injection. However, things have changed with the `inject` function and we can now inject tokens and services outside of class constructors. This provides the ability to define NgRx effects as functions outside of effect classes.

Let's see the functional effects in action!

To create a functional effect, add the `functional: true` flag to the effect config. Then, to inject services into the effect, use the `inject` function.

```ts
import { inject } from '@angular/core';
import { catchError, exhaustMap, map, of, tap } from 'rxjs';
import { Actions, createEffect, ofType } from '@ngrx/effects';

import { UsersService } from './users.service';
import { UsersPageActions } from './users-page.actions';
import { UsersApiActions } from './users-api.actions';

export const loadUsers = createEffect(
  (
    actions$ = inject(Actions),
    usersService = inject(UsersService)
  ) => {
    return actions$.pipe(
      ofType(UsersPageActions.opened),
      exhaustMap(() =>
        usersService.getAll().pipe(
          map((users) =>
            UsersApiActions.usersLoadedSuccess({ users })
          ),
          catchError((error) =>
            of(UsersApiActions.usersLoadedFailure({ error }))
          )
        )
      )
    );
  },
  { functional: true }
);

export const displayErrorAlert = createEffect(
  () => {
    return inject(Actions).pipe(
      ofType(UsersApiActions.usersLoadedFailure),
      tap(({ error }) => alert(error.message))
    );
  },
  { functional: true, dispatch: false }
);
```

> 💡 To make testing easier, it's recommended to inject all dependencies as effect function arguments. However, it's also possible to inject dependencies in the effect function body. In that case, the `inject` function must be called within the injection context.

To register functional effects, pass the dictionary of effects to the `provideEffects` function.

```ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideStore } from '@ngrx/store';
import { provideEffects } from '@ngrx/effects';
 
import { AppComponent } from './app.component';
import * as usersEffects from './users.effects';
 
bootstrapApplication(AppComponent, {
  providers: [
    provideStore(),
    provideEffects(usersEffects),
  ],
});
```

> 💡 In module-based Angular applications, functional effects can be registered using the `EffectsModule.forRoot` or `EffectsModule.forFeature` methods.

Functional effects can be tested like any other function. If all dependencies are injected as effect function arguments, `TestBed` is not required to mock dependencies. Instead, fake instances can be passed as input arguments to the functional effect.

```ts
import { of } from 'rxjs';
 
import { loadUsers } from './users.effects';
import { UsersService } from './users.service';
import { usersMock } from './users.mock';
import { UsersPageActions } from './users-page.actions';
import { UsersApiActions } from './users-api.actions';
 
it('loads users successfully', (done) => {
  const usersServiceMock = {
    getAll: () => of(usersMock),
  } as UsersService;
  const actionsMock$ = of(UsersPageActions.opened());
 
  loadUsers(actionsMock$, usersServiceMock).subscribe((action) => {
    expect(action).toEqual(
      UsersApiActions.usersLoadedSuccess({ users: usersMock })
    );
    done();
  });
});
```

---

## Feature Creator Enhancements 🤩

The `createFeature` function now allows adding additional selectors to the feature object using the `extraSelectors` factory.

```ts
import { createFeature, createReducer } from '@ngrx/store';

import { User } from './user.model';

type State = { users: User[]; query: string };

const initialState: State = {
  users: [],
  query: '',
};

export const usersFeature = createFeature({
  name: 'users',
  reducer: createReducer(initialState, /* case reducers */),
  // 👇 adding extra selectors to the `usersFeature` object
  extraSelectors: ({ selectUsers, selectQuery }) => ({
    selectFilteredUsers: createSelector(
      selectUsers,
      selectQuery,
      (users, query) =>
        users.filter((user) => user.name.includes(query)),
    ),
  }),
});
```

The `extraSelectors` factory can also be used to add derived entity selectors to the feature object in a simple and elegant way.

```ts
import { createFeature, createReducer } from '@ngrx/store';
import { createEntityAdapter } from '@ngrx/entity';

import { User } from './user.model';

const adapter = createEntityAdapter<User>();
const initialState = adapter.getInitialState();

export const usersFeature = createFeature({
  name: 'users',
  reducer: createReducer(initialState, /* case reducers */),
  // 👇 adding entity selectors to the `usersFeature` object
  extraSelectors: ({ selectUsersState }) =>
    adapter.getSelectors(selectUsersState)
});
```

Special thanks to [Armen Vardanyan](https://twitter.com/Armandotrue) for writing documentation for this feature!

---

## Action Group Creator Improvements 👌

We received great feedback on the `createActionGroup` function so far. It provides a better developer experience and significantly reduces lines of code in action files. However, when event names are defined in title case format, it can be challenging to search for unused action creators because their names are automatically generated by camel-casing the event names.

In v16, the `createActionGroup` function provides the ability to define event names in the camel case format as well, so action creators will have the same names as events. This makes it easier to search for their usage within the codebase.

```ts
import { createActionGroup } from '@ngrx/store';

import { User } from './user.model';

export const UsersApiActions = createActionGroup({
  name: 'Users API',
  events: {
    usersLoadedSuccess: props<{ users: User[] }>(),
    usersLoadedFailure: props<{ errorMsg: string }>(),
  },
});

// generated action creators:
const {
  usersLoadedSuccess, // type: "[Users API] usersLoadedSuccess"
  usersLoadedFailure, // type: "[Users API] usersLoadedFailure"
} = UsersApiActions;
```

---

## Enhanced Schematics and Standalone Support 🚀

In the last few major releases, many enhancements and new features have been added to the NgRx framework. However, the `@ngrx/schematics` package hadn't received significant updates.

In version 16, NgRx Schematics have been improved and now use the latest features, such as `createFeature` and `createActionGroup`, thanks to the contributions of [Marco Endres](https://github.com/faragos)!

But that's not all! In addition to these improvements, version 16 also introduces the ability to add NgRx packages to standalone Angular applications using the `ng add` command. For example, we can now add the `@ngrx/store` package with initial configuration to a standalone Angular application by running the following command:

```sh
ng add @ngrx/store
```

Similarly, we can add other NgRx packages, such as `@ngrx/effects`, `@ngrx/store-devtools`, and `@ngrx/router-store` to standalone applications using the `ng add` command.

Huge thanks to [Dmytro Mezhenskyi](https://twitter.com/DecodedFrontend) for contributing to this feature!

---

## Simplified View Model Selectors 🪄

The `createSelector` function now accepts a dictionary of selectors as an input and returns a dictionary of state slices selected by the provided selectors as a result. This feature simplifies the creation of view model selectors by reducing repetitive code in projector functions.

```ts
import { createSelector } from '@ngrx/store';

// before:
export const selectUsersPageViewModel = createSelector(
  selectFilteredUsers,
  selectActiveUser,
  selectQuery,
  selectIsLoading,
  (users, activeUser, query, isLoading) => ({
    users,
    activeUser,
    query,
    isLoading,
  })
);

// after:
export const selectUsersPageViewModel = createSelector({
  users: selectFilteredUsers,
  activeUser: selectActiveUser,
  query: selectQuery,
  isLoading: selectIsLoading,
});
```

---

## New tapResponse Signature 😎

The `tapResponse` operator has a new signature that allows us to provide the observer object as an input argument, along with the `finalize` callback. Using this new signature, we can simplify the code and make it more readable.

```ts
import { inject, Injectable } from '@angular/core';
import { exhaustMap } from 'rxjs';
import { ComponentStore, tapResponse } from '@ngrx/component-store';

import { UsersService } from './users.service';
import { User } from './user.model';

type UsersState = { users: User[]; isLoading: boolean };

@Injectable()
class UsersStore extends ComponentStore<UsersState> {
  private readonly usersService = inject(UsersService);

  readonly loadUsers = this.effect<void>(
    exhaustMap(() => {
      this.patchState({ isLoading: true });

      return this.usersService.getAll().pipe(
        tapResponse({
          next: (users) => this.patchState({ users }),
          error: console.error,
          // 👇 set loading to false on complete or error
          finalize: () => this.patchState({ isLoading: false }),
        })
      );
    })
  );
}
```

---

## Standalone Component APIs 😍

`LetDirective` and `PushPipe` are standalone in version 16, so we can directly add them to the `imports` of a standalone component or NgModule, instead of using `LetModule` and `PushModule`.

```ts
import { Component } from '@angular/core';
import { LetDirective, PushPipe } from '@ngrx/component';
 
@Component({
  // ... other metadata
  standalone: true,
  imports: [
    // ... other imports
    LetDirective,
    PushPipe,
  ],
})
export class UsersComponent {}
```

We would like to thank [Stefanos Lignos](https://twitter.com/stefanos_lig) for contributing to this feature!

---

## New Router Selector 🤘

We've added a new router selector that selects the route data property by name.

```ts
import { createSelector } from '@ngrx/store';
import { getRouterSelectors } from '@ngrx/router-store';

const { selectRouteDataParam } = getRouterSelectors();

export const selectFullNameFromRoute = createSelector(
  selectRouteDataParam('firstName'),
  selectRouteDataParam('lastName'),
  (firstName, lastName) => `${firstName} ${lastName}`
);
```

Many thanks to [Samuel Fernández](https://github.com/samuelfernandez) for contributing to this feature!

---

## NgRx SignalStore 💡

We've recently opened an [RFC](https://github.com/ngrx/platform/discussions/3796) for a new state management solution that will provide first-class support for Angular Signals. The design of the SignalStore APIs is still under consideration, so we'd love to hear your thoughts! If you're interested, take a look and let us know what you think.

---

## 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 16 migration guide](https://ngrx.io/guide/migration/v16) 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 16 🗓️

To start using NgRx 16, make sure to have the following minimum versions installed:

- Angular version 16.x
- Angular CLI version 16.x
- TypeScript version 5.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
```

---

## Enterprise Support 👨‍🏫

The NgRx collection of libraries will always remain open-source, MIT-licensed, and free to use. At the same time, we hear from companies that designing high-quality architecture could be challenging and additional help and guidance are needed.

Participating members of the NgRx team now offer *paid support* for those interested in NgRx workshops, training on best practices, architecture review, and more.

Visit our [Enterprise Support](https://ngrx.io/enterprise-support) page to learn more.

---

## 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. We've recently added a [community section](https://ngrx.io/about?group=Community) to the NgRx team page that lists every person that has contributed to the platform.

We would like to thank the contributors that helped to work towards this release of NgRx: Andreas Pramendorfer, André Escocard, Andrés Villanueva, Armen Vardanyan, Ben Lesh, briannarenni, Cameron Maunder, coder925, CoranH, Cédric Duffournet, Daniel Sogl, Dmytro Mezhenskyi, Fabien Wautriche, Fábio Englert Moutinho, Jerome Wirth, Jobayer Ahmed, jonz94, Jovan Mitrović, Kyler Johnson, Laforge Thomas, Manuel A Martin Callejo, Manuel Geier, Marco Endres, Michael Egger-Zikes, Mike Drakoulelis, naticaceres, Samruddhi Khandale, Samuel Fernández, Soumya Kushwaha, Stefanos Lignos, Taylor Ackley, Thomas Waldbillig, zakaria-bali, zbarbuto.

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 Silver sponsor, [Nx](https://nx.app/company)! Nx has been a longtime promoter of NgRx as a tool for building large 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)!

---

## Sponsor NgRx 💰

We are looking for our next **Gold** sponsor, so if your company wants to sponsor the continued development of NgRx, please visit our [GitHub Sponsors](https://github.com/sponsors/ngrx) page for different sponsorship options, or [contact us](https://ngrx.io/about?group=Core) directly to discuss other sponsorship opportunities.

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.