DEV Community 👩‍💻👨‍💻

Cover image for Building Reactive Angular Templates with NgRx Component
Marko Stanimirović for NgRx

Posted on

Building Reactive Angular Templates with NgRx Component

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

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

💡 PushModule is available since version 14. If you're using an older version of the @ngrx/component package, import the ReactiveComponentModule.

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

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 trigger change detection when an observable emits the same values in a row.
  • 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) {}
}
Enter fullscreen mode Exit fullscreen mode

💡 LetModule is available since version 14. If you're using an older version of the @ngrx/component package, import the ReactiveComponentModule.

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

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

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

💡 Displaying thrown error is possible since version 14. In previous versions, the value of $error is true 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>
Enter fullscreen mode Exit fullscreen mode

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

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

💡 Passing non-observable values to the *ngrxLet directive is available since version 14.


Summary

Many new and powerful features have been added in version 14:

  • Separate modules for LetDirective and PushPipe
  • Displaying emitted error in the template
  • Using aliases for non-observable values
  • Handling suspense state in the template
  • Strong typing for LetDirective and PushPipe

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!

By the way, the @ngrx/component package recently reached 30k downloads per week on NPM! 🎉

ngrx-component-npm

Resources

Peer Reviewers

Big thanks to Brandon Roberts and Tim Deschryver for giving me helpful suggestions on this article!

Top comments (7)

Collapse
mustapha profile image
Mustapha Aouas

I didn’t know about the suspense template.
Thanks for sharing 🙏

Collapse
oz profile image
Evgeniy OZ • Edited on

Agree, this feature alone is enough to start using this library.

Collapse
benlune profile image
Benoît Plâtre

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?

Collapse
markostanimirovic profile image
Marko Stanimirović Author

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

Collapse
markostanimirovic profile image
Marko Stanimirović Author

@benlune

Here is the example of small zone-less app with @ngrx/component v14.1.0: stackblitz.com/edit/angular-zy74xp...

Collapse
benlune profile image
Benoît Plâtre

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

Collapse
markostanimirovic profile image
Marko Stanimirović Author

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.

🌚 Friends don't let friends browse without dark mode.

Sorry, it's true.