Setting Up the Library
Installing @ngrx/component is straightforward. You can either run the package manager command directly in your terminal:
npm install @ngrx/component
Alternatively, you can use the Angular CLI schematic:
ng add @ngrx/component@latest
Once installed, both the directive and the pipe become available through ReactiveComponentModule. Import this module into your application module, and the library's tools are ready for use.
The ngrxPush Pipe
NgrxPush, commonly referred to as the pushPipe, serves as a drop-in replacement for the standard AsyncPipe. The syntax for using it in your templates is nearly indistinguishable from its predecessor:
{{ username$ | ngrxPush }}
<ng-container *ngIf="user$ | ngrxPush as user">
{{ user.firstName + ‘ ‘ + user.lastName }}
</ng-container>
<user-info-component [user]="user$ | ngrxPush"></user-info-component>
To illustrate the practical difference, consider an example where two components share identical logic and templates, with the only variation being the pipe used for extracting values from an Observable. One leverages async, while the other relies on ngrxPush:
At a surface level, both pipes appear to function identically. However, a closer inspection reveals that ngrxPush introduces key improvements in how change detection is managed. In a typical Angular application, both pipes operate in a similar fashion: upon receiving a new value from the data stream, they update the corresponding variable in the view and invoke the markForCheck method to signal that the view requires re-rendering. Yet, markForCheck alone doesn't trigger the actual change detection cycle; it merely flags the component and its ancestors for checking at the next cycle. The mechanism that completes this process and ensures the new value is displayed is NgZone. Without it, the update would be marked but never rendered.
ngrxPush Without Zone.js
In an optimal scenario, the view would automatically react to data changes without needing NgZone or manual intervention. There are also application categories where developers prefer to avoid NgZone altogether, for reasons such as performance optimization. While there are numerous other motivations, they fall outside the scope of this discussion.
What happens when we disable zones in our application? If we follow the guidance provided in the official documentation, we can test this scenario:
As anticipated, the component using asyncPipe ceases to refresh its values in the absence of NgZone. Conversely, the component employing ngrxPush continues to operate seamlessly. This resilience stems from the pipe’s fallback behavior: in a zoneless environment, it switches to using the detectChanges method, which directly triggers the change detection mechanism.
Does this approach come with any trade-offs? Unfortunately, yes. If a single Observable is used in multiple places within a template via ngrxPush, change detection will be triggered once per usage. While each detection cycle is typically fast, this redundancy can accumulate. Is there a way to mitigate this? The answer is affirmative, and it doesn't involve reaching for *ngIf. The other tool introduced in this article, the *ngrxLet directive, offers a solution to this inefficiency.
The *ngrxLet Directive
Before diving into *ngrxLet, consider this: how often have you seen code that employs *ngIf solely to "unpack" an Observable from an async pipe?
<div *ngIf="user$ | async as user">
<div>First name: {{ user.firstName }}</div>
<div>Last name: {{ user.lastName }}</div>
<div>Date of birth: {{ user.dateOfBirth }}
<div *ngIf="userAddress$ | async as address">
Address:
<div>{{ address.street + ' ' + address.building }}</div>
<div>{{ address.postalCode + ' ' + address.city }}</div>
</div>
<div>Phone number: {{ user.phone }}</div>
...
</div>
It's a common pattern, and understandably so. Angular doesn't provide a dedicated mechanism for this use case, so developers improvise with *ngIf. However, this practice has a notable drawback: when the stream emits a falsy value, the element bound to *ngIf is removed from the DOM. In many situations, this behavior is unwanted.
Here, *ngrxLet becomes invaluable. Its key distinctions from *ngIf include:
- The element is always preserved in the DOM, regardless of the value emitted by the data stream.
- The directive can directly unwrap an
Observablewithout requiring an additionalasyncpipe orngrxPush.
Further, *ngrxLet offers support for error and completion notifications from the stream, enabling more granular template handling. And, consistent with ngrxPush, it also functions correctly in zoneless setups.
The example below adapts our earlier application, replacing the multiple ngrxPush usages with a single *ngrxLet. The second component shows the traditional approach with async and *ngIf, the latter hiding the entire ul element when the stream's initial value is null:
Production Viability
It's important to note that, in its early iterations, the library depended on certain change detection features tied to Ivy (see the old source code) that were not part of Angular's public API. This likely contributed to its initial availability being limited to the NgRx repository on GitHub. From version 10 onward, the maintainers shifted towards using the established ChangeDetector API and published the package to the npm registry.
However, a glance at the @ngrx/component documentation reveals an immediate caution:
This package is still experimental and may change during development.
This status prompted a deeper investigation. Unfortunately, the primary roadmap issue for the library (see the GitHub issue) was closed by its main contributor. The project's history indicates that multiple proposed ideas and contributions were rejected, likely leading to a halt in active development.
To ascertain the definitive status and prospects, I reached out directly to a member of the NgRx core team, Alex Okrushko. The crux of his response is that the team cannot guarantee further development; the library might be phased out or potentially removed in the future. He encourages exploration of its capabilities but advises against integrating it into production applications. As a silver lining, Alex also hinted at an alternative NgRx concept for zoneless applications, though it is currently in its nascent stages, with a low development priority and no public details.
Final Thoughts
The @ngrx/component package introduces a refreshing change to the already established NgRx ecosystem, focusing on templating utilities rather than state management. Despite its uncertain trajectory, understanding the problems it seeks to address is valuable. It offers an insightful perspective on the evolving landscape of Angular development, particularly surrounding efficiency and modern change detection strategies.
