In this article, we'll look into the @ngrx/component
library used to build reactive Angular templates in a performant way. It contains a set of declarables that are primarily used for rendering observable events and can work in both zone-full and zone-less mode.
Installation
To install the @ngrx/component
package, run one of the following commands:
// Angular CLI
ng add @ngrx/component
// NPM
npm i @ngrx/component
// Yarn
yarn add @ngrx/component
Push Pipe
The ngrxPush
pipe is used for displaying observable values in the template. To use it, import the PushModule
to an Angular module or standalone component:
import { PushModule } from '@ngrx/component';
@Component({
// ... other metadata
standalone: true,
imports: [
// ... other imports
PushModule,
],
})
export class ProductDetailsComponent {
readonly product$ = this.store.select(selectActiveProduct);
constructor(private readonly store: Store) {}
}
💡
PushModule
is available since version 14. If you're using an older version of the@ngrx/component
package, import theReactiveComponentModule
.
The ngrxPush
pipe is an alternative to the async
pipe and can be used in the following way:
<ngrx-product-form
[product]="product$ | ngrxPush"
></ngrx-product-form>
Similar to the async
pipe, the ngrxPush
pipe returns the last emitted value of the passed observable or undefined
if there are no emitted values. However, there are two key differences compared to the async
pipe:
- The
ngrxPush
pipe will not mark the host component as dirty when an observable emits the same values in a row. - The
ngrxPush
pipe will not mark the host component as dirty when an observable emits values synchronously. - The
ngrxPush
pipe will trigger change detection when an observable emits a new value in zone-less mode.
💡 Since version 14, the
@ngrx/component
package uses the global rendering strategy in both zone-full and zone-less mode. In previous versions, it used the native local rendering strategy in zone-less mode, which caused performance issues.
Let Directive
The *ngrxLet
directive is used for rendering observable events in the template. To use it, import the LetModule
to an Angular module or standalone component:
import { LetModule } from '@ngrx/component';
@Component({
// ... other metadata
standalone: true,
imports: [
// ... other imports
LetModule,
],
})
export class ProductListComponent {
readonly products$ = this.productsService.getProducts({ limit: 10 });
readonly totalCount$ = this.productsService.getTotalCount();
constructor(private readonly productsService: ProductsService) {}
}
💡
LetModule
is available since version 14. If you're using an older version of the@ngrx/component
package, import theReactiveComponentModule
.
The *ngrxLet
directive can be used in the following way:
<ng-container *ngrxLet="totalCount$ as totalCount">
<h2>Products ({{ totalCount }})</h2>
<p *ngIf="!totalCount" class="info-alert">
There are no products.
</p>
</ng-container>
At first, it seems that we can achieve the same result using the *ngIf
directive and async
pipe:
<ng-container *ngIf="totalCount$ | async as totalCount">
<h2>Products ({{ totalCount }})</h2>
<p *ngIf="!totalCount" class="info-alert">
There are no products.
</p>
</ng-container>
However, the *ngIf
directive will only create an embedded view if the totalCount
is not zero (truthy value), but not if it is zero (falsy value). On the other hand, the *ngrxLet
directive will create an embedded view when an observable emits a value, regardless of whether it is truthy or falsy.
Tracking Different Observable Events
The *ngrxLet
directive provides the ability to display different content based on the current observable state. For example, we can display an error alert if an observable emits the error event:
<ng-container *ngrxLet="products$ as products; error as error">
<ngrx-product-card
*ngFor="let product of products"
[product]="product"
></ngrx-product-card>
<p *ngIf="error" class="error-alert">{{ error.message }}</p>
</ng-container>
💡 Displaying thrown error is possible since version 14. In previous versions, the value of
error
istrue
when the passed observable emits the error event.
In addition to error, we can also track the complete event:
<ng-container
*ngrxLet="saveProgress$ as progress; complete as complete"
>
<mat-progress-spinner
[value]="progress"
mode="determinate"
></mat-progress-spinner>
<p *ngIf="complete" class="success-alert">
Product is successfully saved!
</p>
</ng-container>
Combining Multiple Observables
The *ngrxLet
directive can be also used with a dictionary of observables. This feature provides the ability to create a view model object in the template:
<ng-container *ngrxLet="{ products: products$, query: query$ } as vm">
<app-search-bar [query]="vm.query"></app-search-bar>
<app-product-list [products]="vm.products"></app-product-list>
</ng-container>
💡 Combining multiple observables in the template using the
*ngrxLet
directive is available since version 15.
Using Suspense Template
Also, there is an option to pass the suspense template to the *ngrxLet
directive:
<ng-container *ngrxLet="products$ as products; suspenseTpl: loading">
<ngrx-product-card
*ngFor="let product of products"
[product]="product"
></ngrx-product-card>
</ng-container>
<ng-template #loading>
<mat-spinner></mat-spinner>
</ng-template>
The suspense template will be rendered when the passed observable is in a suspense state. In the example above, the loading spinner will be displayed until the products$
observable emits a list of products. When this happens, the loading spinner will be removed from the DOM and products will be displayed.
💡 Using suspense template with the
*ngrxLet
directive is available since version 14.
Using Aliases for Non-Observable Values
In addition to observables and promises, the *ngrxLet
directive can also accept static (non-observable) values as an input argument. This feature provides the ability to create readable templates by using aliases for deeply nested properties:
<ng-container *ngrxLet="productForm.controls.price as price">
<input type="number" [formControl]="price" />
<ng-container *ngIf="price.errors && (price.touched || price.dirty)">
<p *ngIf="price.errors.required">Price is a required field.</p>
<p *ngIf="price.errors.min">Price cannot be a negative number.</p>
</ng-container>
</ng-container>
💡 Passing non-observable values to the
*ngrxLet
directive is available since version 14.
Summary
Many new and powerful features have been added in recent versions:
- Separate modules for
LetDirective
andPushPipe
- Displaying emitted error in the template
- Using aliases for non-observable values
- Combining multiple observables in the template
- Handling suspense state in the template
- Strong typing for
LetDirective
andPushPipe
Also, this library has been almost completely rewritten for better performance. If you haven't used it before, give it a try and let us know your impressions!
In version 15, the @ngrx/component
package is no longer marked as experimental and is now marked as stable. By the way, it recently reached 30k downloads per week on NPM! 🎉
Resources
Peer Reviewers
Big thanks to Brandon Roberts and Tim Deschryver for giving me helpful suggestions on this article!
Top comments (7)
I didn’t know about the suspense template.
Thanks for sharing 🙏
Agree, this feature alone is enough to start using this library.
Hi Marko,
Thanks a lot for this article and your work!
I'm used to work with ngrx since long time, and I would to go fully zoneless. I started a project to test it with ComponentStore, ngrxLet or ngrxPush but it doesn't work, nothing is displayed. I can trace the loaded data (from Drupal) in the console thanks to tap operator, but nothing reacts in the template.
Could you please tell me if there is something special to do ? Or do you have an example of zoneLess small app with ngrx?
Thanks Benoit! :)
I'll be glad to help you. Can you create a reproduction via StackBlitz or GitHub repo? Also, feel free to open an issue with provided reproduction here: github.com/ngrx/platform
@benlune
Here is the example of small zone-less app with @ngrx/component v14.1.0: stackblitz.com/edit/angular-zy74xp...
Hi Marko,
I found a solution, inspired by the angular-movies zoneless project created by rx-angular team (thanks a lot to them!).
github.com/tastejs/angular-movies
My project needs routing, and I had to listen to NavigationEnd event and then trigger an applicationRef.tick() in order to render my data zoneless, like this :
github.com/tastejs/angular-movies/...
Great! Here is another example: ngrx.io/api/component/RenderSchedu...
You can use 'RenderScheduler' to schedule a new change detection cycle on route changes. 'RenderScheduler' is also used by 'LetDirective' and 'PushPipe' to trigger CD.