Building Reactive Angular Templates with NgRx Component
Tags: ngrx, angular, rxjs
Published: 2022-06-19 12:52:31

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:

```sh
// 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:

```ts
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 the [`ReactiveComponentModule`](https://ngrx.io/api/component/ReactiveComponentModule).

The `ngrxPush` pipe is an alternative to the `async` pipe and can be used in the following way:

```html
<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](https://github.com/ngrx/platform/issues/3342).

---

## 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:

```ts
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 the [`ReactiveComponentModule`](https://ngrx.io/api/component/ReactiveComponentModule).

The `*ngrxLet` directive can be used in the following way:

```html
<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:

```html
<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:

```html
<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` is `true` when the passed observable emits the error event.

In addition to error, we can also track the complete event:

```html
<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:

```html
<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:

```html
<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:

```html
<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` and `PushPipe`
- 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` 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!

---

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! 🎉

![ngrx-component-npm](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6w626rfyg20g08sfe151.jpg)

## Resources

- [Official docs of the `@ngrx/component` package](https://ngrx.io/guide/component)

## Peer Reviewers

Big thanks to [Brandon Roberts](https://twitter.com/brandontroberts) and [Tim Deschryver](https://twitter.com/tim_deschryver) for giving me helpful suggestions on this article!