Announcing NgRx v16: Integration with Angular Signals, Functional Effects, Standalone Schematics, and more!

markostanimirovic

Marko Stanimirović

Posted on May 9, 2023

Announcing NgRx v16: Integration with Angular Signals, Functional Effects, Standalone Schematics, 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.


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.

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

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.

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

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.

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

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.

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

💡 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.

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

💡 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.

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

Feature Creator Enhancements 🤩

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

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

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

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

Special thanks to Armen Vardanyan 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.

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

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!

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:

ng add @ngrx/store
Enter fullscreen mode Exit fullscreen mode

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

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

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.

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

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.

import { Component } from '@angular/core';
import { LetDirective, PushPipe } from '@ngrx/component';

@Component({
  // ... other metadata
  standalone: true,
  imports: [
    // ... other imports
    LetDirective,
    PushPipe,
  ],
})
export class UsersComponent {}
Enter fullscreen mode Exit fullscreen mode

We would like to thank Stefanos Lignos for contributing to this feature!


New Router Selector 🤘

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

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

Many thanks to Samuel Fernández for contributing to this feature!


NgRx SignalStore 💡

We've recently opened an RFC 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 for complete information regarding migrating to the latest release. The complete CHANGELOG 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:

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

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 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 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. We've recently added a community section 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 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! 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!


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 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 May 9, 2023

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

Sign up to receive the latest update from our blog.

Related