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) {}
}
💡
PushModuleis available since version 14. If you're using an older version of the@ngrx/componentpackage, 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
ngrxPushpipe will not mark the host component as dirty when an observable emits the same values in a row. - The
ngrxPushpipe will not mark the host component as dirty when an observable emits values synchronously. - The
ngrxPushpipe will trigger change detection when an observable emits a new value in zone-less mode.
💡 Since version 14, the
@ngrx/componentpackage 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) {}
}
💡
LetModuleis available since version 14. If you're using an older version of the@ngrx/componentpackage, 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
erroristruewhen 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
*ngrxLetdirective 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
*ngrxLetdirective 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
*ngrxLetdirective is available since version 14.
Summary
Many new and powerful features have been added in recent versions:
- Separate modules for
LetDirectiveandPushPipe - 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
LetDirectiveandPushPipe
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.