DEV Community

Cover image for Announcing NgRx v15: Standalone APIs, Type-safe projectors, Component and ComponentStore updates, and more!
Brandon Roberts for NgRx

Posted on

Announcing NgRx v15: Standalone APIs, Type-safe projectors, Component and ComponentStore updates, 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.


Standalone APIs for NgRx Libraries 🧙

Angular v14 introduced standalone features for components, pipes, and directives allow you to build Angular applications with NgModules primarily being optional. In Angular v15, the standalone APIs are no longer in developer preview.

To improve support for standalone features, all NgRx libraries that register functionality in Angular have an equivalent provide* API for compatibility with building Angular applications with standalone features. Let's take a look at the new APIs.

Root-level providers:

boostrapApplication(AppComponent, {
  providers: [
    provideStore(),
    provideEffects(AppEffects),
    provideStoreDevtools(),
    provideRouterStore(),
    provideEntityData(config, withEffects())
  ]  
});
Enter fullscreen mode Exit fullscreen mode

In lazy-loaded features, such as route configs:

[
  {
    path: 'products',
    component: ProductsPageComponent,
    providers: [
      provideState(productsFeature),
      provideEffects(ProductsEffects)
    ]
  }
]
Enter fullscreen mode Exit fullscreen mode

Thanks to Jay Bell, Santosh Yadav, and others for their help in contributing to these new APIs.

Check out Tim's latest blog post on how NgRx APIs has evolved over time and how you can take advantage of them.


Announcing 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 interest in NgRx workshops, training on best practices, architecture review, and more.

Visit our Enterprise Support page to learn more.


NgRx Component marked as stable ✍

The NgRx Component package brings more reactivity to templates in Angular, along with providing support for fully zone-less Angular applications. NgRx Component provides two main features, the ngrxPush pipe and ngrxLet directive for more easily using observables in Angular component templates.

In version 14, team member Marko Stanimirović completely rewrote the component package from the ground up to provide a more consistent way to handle observable and static data, improved performance for zone-less applications in Angular, and more. In version 15, the component package is no longer marked as experimental and is now marked as stable for developers to take advantage of with full support.

A new feature also added to @ngrx/component is the ability to pass a dictionary of observables in the template with the ngrxLet directive. A view model object is created from the observables provided.

<ng-container *ngrxLet="{ users: users$, query: query$ } as vm">
  <app-search-bar [query]="vm.query"></app-search-bar>
  <app-user-list [users]="vm.users"></app-user-list>
</ng-container>
Enter fullscreen mode Exit fullscreen mode

Read this post to learn more about NgRx Component.


Strict Type-safety For the Selector Projector 🔒

When testing selectors built with NgRx Store, the projector function is a way to provide inputs to a selector and assert against the result. Previously the projector function allowed the arguments passed to it to be less strict, and was not inferred from the selector itself. With version 15, the projector function is now strictly typed out of the box, allowing for more type safety about the inputs passed into the selector.

The projector is strict by default, but can be bypassed with an any type assertion to specify a less specific type.

const mySelector = createSelector(
  () => 'one',
  () => 2,
  (one, two) => 3
);

// Type is projector(s1: string, s2: number): number
mySelector.projector(); // <-- Results in type error.
Enter fullscreen mode Exit fullscreen mode

To opt-out of the new behavior, cast the projector function itself.

const mySelector = createSelector(
  () => 'one',
  () => 2,
  (one, two) => 3
)

(mySelector.projector as any)();
Enter fullscreen mode Exit fullscreen mode

Thanks to David Shortman for contributing to this feature.

Simplified View Models with ComponentStore ✨

Creating view models is a recommended way to consolidate multiple streams in a clean API to provide to your components. ComponentStore allows you to easily compose these streams from multiple sources. To further improve this, we've added a new signature to the ComponentStore.select method that allows you to provide a dictionary of observables, and an observable dictionary of those values is returned.

Before the new method, the ComponentStore projector was a repeat of its inputs.

readonly vm$: Observable<SomeViewModel> = this.select(
  this.selectedTab$,
  this.routerStateService.baseListRoute$,
  this.secondaryNavigationItems$,
  this.isFlaggedPage$,
  this.pageTitle$,
  this.hasError$,
  this.errorMessage$,
  (
    selectedTab,
    baseRoute,
    secondaryNavTabs,
    isFlaggedPage,
    pageTitle,
    hasError,
    errorMessage,
  ) => ({
    selectedTab,
    baseRoute,
    secondaryNavTabs,
    isFlaggedPage,
    pageTitle,
    hasError,
    errorMessage,
  }),
  { debounce: true },
); 
Enter fullscreen mode Exit fullscreen mode
  • The new select signature makes this much more concise.
readonly vm$: Observable<SomeViewModel> = this.select({
  selectedTab: this.selectedTab$,
  baseRoute: this.routerStateService.baseListRoute$,
  secondaryNavTabs: this.secondaryNavigationItems$,
  isFlaggedPge: this.isFlaggedPage$,
  pageTitle: this.pageTitle$,
  hasError: this.hasError$,
  errorMessage: this.errorMessage$,
}, { debounce: true }); 
Enter fullscreen mode Exit fullscreen mode

We also plan to add support for providing a dictionary to selectors in NgRx 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 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.


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 15 migration guide for complete information regarding migrating to the latest release. The complete CHANGELOG can be found in our GitHub repository.


Upgrading to NgRx 15 🗓️

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

  • Angular version 15.x
  • Angular CLI version 15.x
  • TypeScript version 4.8.x
  • RxJS version ^6.5.x or RxJS version ^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

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.

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.


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.

We want to give a big thanks to our Silver sponsor, Narhwal Technologies! Nrwl 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 for Nx.

Follow us on Twitter for the latest updates about the NgRx platform.

Oldest comments (8)

Collapse
 
santoshyadavdev profile image
Santosh Yadav

Thank you for mentioning my name here 💙

Collapse
 
brandontroberts profile image
Brandon Roberts

Always appreciated Santosh 💙

Collapse
 
baluditor profile image
Baluditor

Does this release includes the support for tracing with the Redux DevTools? Anyhow, amazing work you guys do.

Collapse
 
brandontroberts profile image
Brandon Roberts

Yes that PR did make it into the release

Collapse
 
baluditor profile image
Baluditor

Thanks, will check it out!

Collapse
 
davidshortman profile image
David

Hooray for new API support for standalone APIs and Observables!

Collapse
 
brandontroberts profile image
Brandon Roberts

Thanks for your contributions!

Collapse
 
pbouillon profile image
Pierre Bouillon • Edited

Hey! All those features are really interesting, especially the simplified View Models with ComponentStore

However, when using the new syntax all my view models result in being null

As an example I have:

readonly product$ = this.select(store => store.product)

readonly vm$ = this.select(
  product$,
  (product) => ({ product }));
Enter fullscreen mode Exit fullscreen mode

Which I migrated to:

readonly product$ = this.select(store => store.product)

readonly vm$ = this.select({
  product: this.product$
});
Enter fullscreen mode Exit fullscreen mode

Any idea why?