Search code examples
javascriptangularangular-state-managmement

Angular re-renders list of components when state is changed without mutation


I am coming from React and I've been working with Angular for a few months now. A React component updates only the necessary HTML when is re-rendered, while an Angular component seems to re-render completely when the state is not mutated. If I mutate the state directly seems to work just fine.

Here's my very basic example:

I have the main component which holds the state:

items = [{ name: "John", age: 8 }, { name: "Jane", age: 20 }];

and in its view it renders a list of items:

<item *ngFor="let item of items" [item]="item"></item>

the item component looks like this:

@Component({
  selector: "item",
  template: `
    <p>
      {{ item.name }}, {{ item.age }}
      <input type="checkbox" />
    </p>
  `
})
export class ItemComponent {
  @Input() item: any;
}

I added that checkbox in the item component just to notice when the component is fully re-rendered.

So what I do is tick the checkboxes and increment the age for each user in the state.

If I increment it by mutating the state the view is updated as expected and the checkboxes remain checked:

this.items.forEach(item => {
  item.age++;
});

But if I change the age without directly mutating the state the entire list is re-rendered and the checkbox is not checked anymore:

this.items = this.items.map(item => ({
  ...item,
  age: item.age + 1
}));

Here's a full example in a CodeSandbox.

Can anyone please explain why is this happening and how can I make the list to not fully re-render when I don't mutate the state?


Solution

  • NgForOf directive which Angular uses to render list of items checks if there are any changes in array we pass to it.

    If you mutate property in items then Angular knows that there is no changes since references to objects in array remain the same.

    On the other hand, if you completely replace the previous object with new one then it is signal for ngForOf to rerender that object.

    To determine if an object changes ngForOf directive use DefaultIterableDiffer that looks at trackByFn function if we provided it. If we don't provide it then the default function looks like:

    var trackByIdentity = function (index, item) { 
      return item;
    };
    

    And then Angular compares result of this function with previous value in collection.

    You can think about trackByFn like key in React.

    So your solution might look like:

    *ngFor="let item of items; trackBy: trackByFn"
    
    trackByFn(i) {
        return i;
    }
    

    https://codesandbox.io/s/angular-67vo6