Announcing NgRx v16: Integration with Angular Signals, Functional Effects, Standalone Schematics, and more!
Marko Stanimirović
Posted on May 9, 2023
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);
}
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))
);
}
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()))
);
}
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 }
);
💡 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),
],
});
💡 In module-based Angular applications, functional effects can be registered using the
EffectsModule.forRoot
orEffectsModule.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();
});
});
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)),
),
}),
});
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)
});
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;
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
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,
});
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 }),
})
);
})
);
}
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 {}
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}`
);
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
If your project uses @ngrx/component-store
, but not @ngrx/store
, run the following command:
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 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.
Posted on May 9, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.