Best practices for RxJS inside non-trivial Angular components

Our team is in the process of moving from AngularJS to Angular (4).

Inside our Angular components, we use the RxJS library to work with observable data returned by Http, ActivatedRoute, etc., or entities that receive values ​​from inputs on the user interface. When some processing of the values ​​obtained by these observables is required, we compare them or combine them into new observables to obtain “received” observables. Nothing special here.

As a result, we get components in which almost all the public properties associated with templates are observable.

However, this method has serious disadvantages:

  • This requires very good knowledge and understanding of the RxJS library, and many tasks that used to seem simple have become very complex, with a lot of time debugging / optimizing subscriber signatures.
  • Our templates are now filled with asynchronous channels, and we regularly run Angular errors like this one: https://github.com/angular/angular/issues/10165 .

To give you an example, here is a simple case (not the worst, of course): the pagination component template. Pay attention to the large number of asynchronous pipes, some of which are nested inside the structural directive, asynchronous itself (which causes the error that I just mentioned).

<nav aria-label="Pagination" class="mt-5" *ngIf="($items | async)?.length"> <ul class="pagination"> <li class="page-item" [class.disabled]="(currentPage$ | async) === 1"> <a class="page-link" [routerLink]="" [queryParams]="{ page: (currentPage$ | async) - 1 }" queryParamsHandling="merge">Previous</a> </li> <li *ngFor="let page of (pages$ | async)" class="page-item" [class.active]="page === (currentPage$ | async)"> <a class="page-link app-page-number" [routerLink]="" [queryParams]="{ page: page }" queryParamsHandling="merge">{{ page }} <span class="sr-only" *ngIf="page === (currentPage$ | async)">(current)</span></a> </li> <li class="page-item" [class.disabled]="(currentPage$ | async) === (lastPage$ | async)"> <a class="page-link" [routerLink]="" [queryParams]="{ page: (currentPage$ | async) + 1 }" queryParamsHandling="merge">Next</a> </li> </ul> </nav> 

Changing the "output" observables to simple (non-observable) properties populated from .subscribe () helps reduce code complexity as well as the number of asynchronous channels within the template. But this does not seem to be a satisfactory solution.

In addition, we are facing some new issues related to detecting changes using the OnPush strategy. Since we turned our observables into “normal” properties and removed the asynchronous pipes, Angular no longer knows that we are changing these properties, and we often have to call ChangeDetectorRef.detectChanges ().

So far, we have not been able to find clear recommendations for working with RxJS inside non-trivial components. Have you had any suggestions on how to get around these difficulties? Could Redux be the solution to some (our) problems?

Edit: It turned out that the above “error” is actually a misuse of the RxJS library on our part. See My Contribution to ( GitHub Question ) for more details.

+7
angular redux observable rxjs
source share
1 answer

Change detection

The *ngIf="(obs | async)" problem is one way to detect changes. The obs variable itself does not change when the radiation, in combination with the fact that this expression is in the template, makes it difficult to detect changes for detection.

In this scenario, there are a few principles to consider:

  • The observed variables are "pipes", i.e. wrappers for values ​​passing through them, not the changing values ​​themselves. These two are often equated, but it looks like the elements of an array and an array are the same thing.

  • RxJs is an "external" (non-w500>) library. In these libraries, we need to know if changes in the data processed by the library are visible on Angular change detection.

Fix for # 10165

 <div style="background-color: green;" *ngIf="trigger">{{(val1 | async)}}</div> <div style="background-color: green;" *ngIf="!trigger">{{(val2 | async)}}</div> ngOnInit() { this.trigger = this.ifObservable.subscribe(); } 

Work with asynchronous data

In the broader issue of “observable everywhere,” you are perfectly correct, but is it not a problem inherent in the asynchronous nature of web applications?

It would be interesting to compare your old AngularJS code with the new Angular code. Is there an increase in complexity for the same feature set?

The principle that I would like to apply is to process “one-time” observables, such as http.get , by subscribing to them, and to keep “multi-capture” observables connected via asynchronous channels.

Onpush

This is, in fact, a way to dial the amount of automatic change detection, and therefore speed up the application. Of course, this means that you may have to exchange detection of fire changes more often - a compromise between speed and compromise.

Redux

While Redux repositories typically display state as observable (so you still need an asynchronous channel in the template), it eliminates the complexity around multiple update points, which may mean that less observables are required (or at least observables are abstracted from components).

The only shade of Redux that I looked at that does not include deasynchronous data in the template is Mobex , which essentially converts simple variables into objects with getters and setters that contain logic for viewing asynchronous changes and deploying them. But it has problems / caveats with arrays.

Simplification of the template

One way that you could simplify the pattern you showed (I have not tested this) is to wrap it in a child component and pass the expanded values

 <nav aria-label="Pagination" class="mt-5" *ngIf="items.length"> <ul class="pagination"> <li class="page-item" [class.disabled]="currentPage === 1"> <a class="page-link" [routerLink]="" [queryParams]="{ page: currentPage - 1 }" queryParamsHandling="merge">Previous</a> </li> <li *ngFor="let page of pages" class="page-item" [class.active]="page === currentPage"> <a class="page-link app-page-number" [routerLink]="" [queryParams]="{ page: page }" queryParamsHandling="merge">{{ page }} <span class="sr-only" *ngIf="page === currentPage">(current)</span></a> </li> <li class="page-item" [class.disabled]="currentPage === lastPage"> <a class="page-link" [routerLink]="" [queryParams]="{ page: currentPage + 1 }" queryParamsHandling="merge">Next</a> </li> </ul> </nav> export class PageComponent { @Input() currentPage = 0; @Input() pages = []; @Input() items = []; 

and in the parent,

 <page-component [pages]="pages$ | async" [items]="items$ | async" [currentPage]="currentPage$ | async" > 

By default, @Input connects to change detection even with the onPush strategy.
Be careful to give child values ​​default values.

+1
source share

All Articles