Home > front end >  Angular OnPush strategy not working with ngFor - always recreates all children
Angular OnPush strategy not working with ngFor - always recreates all children

Time:02-10

i am trying to build a small framework that renders forms based on provided JSON data which is describing that form's structure. Each form element specified in the data is represented by an angular component at runtime.

Elements in the form can be nested using 'container' form elements. The corresponding component is using an *ngFor loop on an array.

I chose to use OnPush Change Detection which works well - except for the container components which will always recreate all child elements when an array item changes, so when change detection is done on the @inputs, there is nothing to compare to.

I tried to make a reduced example here, hope you get the point.

https://stackblitz.com/github/rpnoll1977/TestNgArray

To be able to tell which parts get recreated, i added some input fields. You can type in some random text. When you hit the button, a new element is added to the list. All elements in the list get recreated - not the others, especially not the input which is in parallel to the *ngfor.

I was able to debug this a bit after providing a tracking function in the *ngFor. In Angular's DefaultIterableDiffer.check it always sets mayBeDirty=true as the record to compare to is equal to null.

Actually i made another example which, instead of *ngfor uses a series of *ngIfs (with conditions about the length of the array being smaller than the position of the *ngif). That shows correct behavior, only re-rendering the added item.

CodePudding user response:

You should pay attention on how you're updating your model and how you use trackBy feature.

You're expecting that items in model for ngFor stay the same.

tracker(index:number, item:any):any{            
  return item;    
}

That's true for deeply nested items:

controls: [
  ...this.model.controls[0].controls, <=== they stay the same
  {id:"labelX", type:"l"},  
]      


this.model.controls[0].controls[0] === newModel.controls[0].controls[0]); => true

But that's not true for one container above:

var newModel = {
  ...this.model, 
  controls: [ <=== here you create a completely new array with new objects
    {
    ...


this.model.controls[0] === newModel.controls[0] // false

So the solution is to use some identificator for trackBy which is id in your case:

tracker(index:number, item:any):any{            
  return item.id;    
}

Forked Stackblitz

CodePudding user response:

Your tracker function used in trackBy function is wrong

it is

  tracker(index:number, item:any):any{            
    return item;/// wrong
  }

while it should be

https://stackblitz.com/edit/github-qhynwq?file=src/app/app.component.ts

  tracker(index:number, item:any):any{            
    return item.id;
  }
  • Related