6

The question

Is Angular designed to constantly re-check everything in order to detect changes? I'm coming from the React world and I was expect something like event triggered -> re-rendering. I don't know if this is something from my app or something from Angular.

If I have a method that's called from the HTML Template, it gets called infinitely even if nothing in my component changes.

The problem

I have a calendar-like page that loads a lot of data and has to compute things before rendering, because it would be too difficult to do these things with the ngIf directive. So I have things in my template that look like:

<div [innerHTML]="_getDayPrice(item, day) | safeHtml"></div>  

If I console log something in the _getDayPrice method, it gets printed infinitely.

What I've tried

  1. I've managed to bypass this by manually injecting the ChangeDetectionRef in my app and doing this.cdRef.detach(). This however feels hacky as sometimes I might need to re-enable it and detach again.

  2. I've tried to investigate if it's something from my parent component apps, like containers. I've rendered in my main app.component a single div like <div class={{computeClass()}}> and in that method printed a console log and sure enough it gets called infinitely. So following this I've tried commenting-out all of the apps' services. If all are commented out sure enough it works properly, but also there's no observable data. I've investigated for about half a day and couldn't find a single point of failure (like commenting out this service fixes everything).

  3. Record performance using chrome's built-in Performance tab, but again couldn't find anything from my code that triggers changes. zone.js gets called repeatedly and appears to set an interval that keeps firing.

  4. Of course I've searched for occurrences of setTimeout and setInterval in the services but couldn't find something that keeps getting changed that might cause this issue.

Conclusion?

Bottom line is: is it normal, if you have a complex Angular app and call a method from the template, for that method to be called infinitely?

And if not, do you have any hints as to what might be causing this? Or any other means to bypass it rather than detaching the changeRef detector?

My only concern is with performance. It's a calendar-like page that renders multiple-rows, and it lags pretty severely on a 8GB RAM laptop. I'm pretty sure that a tablet or a phone would almost freeze.

Raul Rene
  • 10,014
  • 9
  • 53
  • 75
  • Yes, this is expected behavior. You should cache the value or build it one of the life cycle events like ngOnInit. – Igor Jun 25 '18 at 11:07
  • @Igor This might work. The thing is that for each item row I might have tens or hundreds of cells (days). And for each day I have to render a HTML in that cell. Doing all these computations on component load and caching in-memory such a big load of data feels weird. – Raul Rene Jun 25 '18 at 11:09
  • 3
    Try to avoid binding functions to inputs like this, for this exact reason. If you absolutely have to, try putting the above code in a component with onPush change detection. so it only initiates change detection when an input has changed. – JoshSommer Jun 25 '18 at 11:11
  • 1
    Then I recommend caching the values. You could use a private lookup/dictionary to keep each value set in. If the value exists return it, otherwise build it and assign it to the lookup object. – Igor Jun 25 '18 at 11:11
  • For more about how to use expression binding, reamd the official guide here: https://angular.io/guide/template-syntax#expression-guidelines – ForestG Jun 25 '18 at 11:33
  • If `_getDayPrice` doesn't call a web service you want to create a proper component instead of what you are doing now. – a better oliver Jun 25 '18 at 13:00

2 Answers2

13

You should use pipes as often as possible when transforming data in your HTML.

Pipes are only re-evaluated when the piped object or parameters change (so if one of the inputs is an object, make sure to create a new instance of that object to trigger re-evaluation). For most uses, you can use a pipe rather that an function.

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'getDayPrice'
})
export class GetDayPricePipe implements PipeTransform {

  transform(item: string, day: string): string {
    // ... do whatever logic here
    return item + day;
  }

}

Then use it like that :

<div [innerHTML]="item | getDayPrice:day | safeHtml"></div>
JusuVh
  • 166
  • 6
  • 1
    I would add upon the fact that pipes only re evaluate when parameters change, that it depends on the pipe purity ( see https://angular.io/guide/pipes#pure-and-impure-pipes ) – Pierre Mallet Jun 25 '18 at 15:16
  • Wow! Thanks a lot! Using a pipe did the trick. I also console logged it and only enters the pipe transform once. – Raul Rene Jun 25 '18 at 17:18
  • 2
    More on pipes, I suggest watching a 5 min youtube video : 'Increasing Performance - more than a pipe dream' by Tanner Edwards in ng-Conf – JusuVh Jun 26 '18 at 07:23
4

There are two mechanisms availables to mitigate number of bindings checks

1 - ChangeDetectorRef.detach : Allow to detach your compoentn out of the change detection loop, so the bidding while not be refreshed until reattachement.

2 - ChangeDetectionStategy.OnPush : Tell Angular that the bindings of your component need to be checked only when at least one of the component @Input has changed. Your can find me details on how to use it here

IMO you should create a component with item / days Inputs wrapping your display DOM element. Something like

// wrap.component.ts
@Component({
  template: '<div [innerHTML]="_getDayPrice() | safeHtml"></div>',
  changeDetection: ChangeDetectionStrategy.OnPush,
  ...
})
export class WrapComponent {
  @Input() item: Item;
  @Input() day: String; 

  private _getDayPrice = () => {
      // compute your HTML with this.item and this.day instead of parameters
      ....
  }
}

Then you should see the logic triggered only when the item or the day changes.

Pierre Mallet
  • 7,053
  • 2
  • 19
  • 30
  • Hey, thanks for your response. I'll give it a go tomorrow. I have a question though: I've detached the Change Ref and I still got infinite `console.logs`. Any reason why this might happen? Shouldn't the component only get rendered once? – Raul Rene Jun 25 '18 at 14:14
  • I though you said that "I've managed to bypass this by manually injecting the ChangeDetectionRef in my app and doing this.cdRef.detach()". But if you still have calls to _getDayPrice() after cdref detach its weird and i think the OnPush stategy might also not work. – Pierre Mallet Jun 25 '18 at 15:19
  • In managing I saw a far better performance improvement, the same I saw with setting `OnPush` strategy, but I still get calls. That's the weirdest part. It works 80% decent now but I still get a little lag on things like hover which you can tell don't happen instantaneously. I'm thinking of creating a sub-component for each cell and passing the item and day as `Inputs` as you've suggested and set onPush for that, maybe it'll help. – Raul Rene Jun 25 '18 at 16:50