1

I've been conducting a little experiment related to handling state in an Angular component.

Let's say I have a variable costly to compute that depends on several others. I can write a function updateVar() and call it on every event I know may impact it, eg. via subscriptions. But it doesn't feel very DRY nor robust when aforementioned function is called in more than 5 different places.

So instead, how about debouncing the DoCheck Angular lifecycle hook ?

docheckSubject: Subject<any> = new Subject();
debounced: boolean = false;

constructor(private cd: ChangeDetectorRef) {}

ngOnInit() {
  this.docheckSubject.debounceTime(50).subscribe(
    () => this.ngDoCheckDebounced()
  );
}

ngDoCheck() {
  if (!this.debounced) {
    this.docheckSubject.next();
  } else {
    this.debounced = false;
  }
}

ngDoCheckDebounced() {
  this.debounced = true;
  this.updateVar();
  this.cd.detectChanges();  // needed to reflect update in DOM
}

Here's a plunker example.

This approach seems to work fine in my real app, where of course way more ngDoCheck() are happening, and Protractor tests don't complain either. But I can't shake off the feeling of doing a dirty not-so-clever hack.

Question: Is this going to bite me ? In fact, do I need it at all ?

Community
  • 1
  • 1
Arnaud P
  • 12,022
  • 7
  • 56
  • 67
  • `ngDoCheck` is called each time the component is checked, what's the point of debouncing it? maybe figure out why the check is conducted so often? – Max Koretskyi Aug 03 '17 at 16:30
  • It's called so often because the component has an array of sub-components of varying number (let's say~20 on average) that contain forms. On user selection, all sub-components forms may be patched to new values. As far as I can tell this can lead to ~50 ngDoCheck() calls in a row. – Arnaud P Aug 03 '17 at 16:34
  • BTW I did read your other posts, eg. https://stackoverflow.com/questions/42643389 ;) – Arnaud P Aug 03 '17 at 16:35
  • cool) hope it helped you. what do you do in `ngDoCheck`? and how does patching sub-components forms affect the parent component state? – Max Koretskyi Aug 03 '17 at 16:37
  • The parent component holds a 'main' form (the pristine state of which I am interested in), that contains a formArray. This formArray is passed to child components, and they add they own form to it. The idea was that any child becoming dirty will dirty the main form. – Arnaud P Aug 03 '17 at 16:41

3 Answers3

1

ngDoCheck is called extremely often:

https://angular.io/guide/lifecycle-hooks#docheck

This hook is called with enormous frequency—after every change detection cycle no matter where the change occurred.

It's a bad idea to use this hook for anything other than variables that Angular doesn't know to look for change detection. (For instance, an input object changing the value of one of its properties). Debouncing helps somewhat, but you're still executing that logic extremely often, and will continue to do so after you no longer need it (i.e. when debounced == false).

I think your solution will work, but I think the overhead with this approach is considerably worse than the alternative. It might be worth putting the update logic as part of the Observable chain, or passing your Observables into a function that adds this logic into the chain.

chrispy
  • 3,552
  • 1
  • 11
  • 19
  • If you try the plunker, you'll see that debouncing is always turned on. The `debounced` variable is just a guard to prevent infinite looping. It's supposed to always be `false`, unless the change was triggered by my debounced function. As for the perf, In my 'real' examples ngDoCheck() can be called 50x times more often that the debounced function, which is quite a relief. – Arnaud P Aug 03 '17 at 16:44
  • About the Observable chain, that's precisely my point, in this case I've got to put the update in quite a number of chains, because it can come from several unrelated user actions. I was wondering about alternatives. – Arnaud P Aug 03 '17 at 16:49
  • I only meant that the code would continue to be executed, sorry for being unclear. One potential downside of this is that if the user continues to trigger DoChecks unknowingly, then the debounce will not resolve until they stop and they won't see an updated `var`. – chrispy Aug 03 '17 at 16:49
  • Ah yes, good point, if ngDoCheck() keep happening within an interval of 50ms (in this ex.), I am in trouble. Missed that bit in the rx specs, thanks for the heads up. – Arnaud P Aug 03 '17 at 16:54
  • Not a problem! I think this is an interesting solution, and not really a `hack` -- I just think it's probably not the best approach. – chrispy Aug 03 '17 at 16:57
  • Now come to think of it, I don't think a normal user can input actions at this rate for a very long time :) So it would only come from a setInterval() or something. That is to say, **I** would have been looking for trouble. In which case, my protractor tests will scream. – Arnaud P Aug 03 '17 at 16:58
  • ngDoChecks don't only come from user actions -- complex pages will call this hook extremely often from anything that triggers a change detection cycle, like hovers, mousemoves, and global DOM events. – chrispy Aug 03 '17 at 17:02
1

After several months in production with no issue (AFAICT), I ended up removing it because it eventually caused an e2e (protractor) test to block until someone would manually click on the page. Obviously not sustainable.

I couldn't properly diagnose the cause of this issue, it may have originated from a quirk in an unrelated part of our code, but better safe than sorry.

Arnaud P
  • 12,022
  • 7
  • 56
  • 67
0

Could you put the variable in a getter and only recalculate when it is retrieved?

Here is a simple example:

//our root app component
import {Component, NgModule, VERSION} from '@angular/core'
import {BrowserModule} from '@angular/platform-browser'

@Component({
  selector: 'my-app',
  template: `
    <div>
      <h2>Hello {{name}}</h2>
      <div>x={{x}}</div>
      <div>y={{y}}</div>
      <button (click)="increment()">Increment</button>
      <div>Calculated Value={{calculatedValue}}</div>
    </div>
  `,
})
export class App {
  name:string;
  x: number = 0;
  y: number = 10;

  get calculatedValue(): number {
    return this.x * this.y;
  }

  constructor() {
    this.name = `Angular! v${VERSION.full}`
  }

  increment(): number {
    this.x++;
  }

}

And associated Plunker: https://plnkr.co/edit/QC7mkbsbjRRBjJOjWSHe?p=preview

DeborahK
  • 57,520
  • 12
  • 104
  • 129
  • I don't think I can, since the state I'm updating is bound to the DOM via interpolation (`{{var}}`), as well as passed down to child components (`[var]="var"`). In fact, that's precisely why I felt the need to use a stateful variable, because AFAICT binding to a heavy method is tantamount to calling it in `ngDoCheck()`, times the number of its occurrences in the template. – Arnaud P Aug 03 '17 at 18:13