55

I have an array of objects (arr). In one of my component's inputs, in the (change) method, I modify the attribute of one of the objects but in the view (*ngFor) nothing changes. I read that Angular’s change detection doesn't check the contents of arrays or objects, so I’ve tried these:

this.arr = this.arr.slice();

this.arr = [...this.arr];

But the view doesn't change, it still shows the old attribute. In the (change) method with console.log() I got the correct array. Weird, but this one works: this.arr = []; (I’ve tried NgZone and markForCheck() too.)

Roland Rácz
  • 2,879
  • 3
  • 18
  • 44

11 Answers11

36

You could also use the trackBy option in your *ngFor expression, providing a unique ID for every item inside the array. This does make you 100% responsible for change detection, so update this (unique) property every time the item in the array changes. Angular will then only re-render the list if any item inside your array has been given a different trackBy property:

*ngFor="let item of (itemList$ | async); trackBy: trackItem"

or:

*ngFor="let item of itemList; trackBy: trackItem"

where:

trackItem is a public method in your component:

public trackItem (index: number, item: Item) {
  return item.trackId;
}
blacktide
  • 10,654
  • 8
  • 33
  • 53
Michahell
  • 4,905
  • 5
  • 29
  • 45
35

Since you mentioned that you already tried markForCheck(), you should try detectChanges instead (that just what worked for me and markForCheck did not work). For those that need the steps:

Add ChangeDetectorRef to your imports:

import { Component, OnInit, ChangeDetectorRef } from '@angular/core';

Add ChangeDetectorRef to your constructor:

constructor(
    private changeDetection: ChangeDetectorRef
  ) { }

Then on the next line after you update the array:

this.changeDetection.detectChanges();
Kyle Krzeski
  • 6,183
  • 6
  • 41
  • 52
6

late to the game but creating a deep clone using lodash worked for me

this.arr = cloneDeep(this.arr);

https://lodash.com/docs/4.17.15#cloneDeep

cup_of
  • 6,397
  • 9
  • 47
  • 94
  • 2
    don't need lo-dash for this. `this.arr = this.arr.map(el => Object.assign({}, el)` or `this.arr = [...this.arr]` it it's an array of primitives. – dman Aug 09 '20 at 18:19
4
  1. Check if your component is configured with changeDetection:cHangeDetectionStrategy.OnPush, if you are going this then after updation of array you have to call changeDetectorRef.markForCheck()
  2. You can also implement onChange lifecycle hook and change values of array inside this function.
Rohan Fating
  • 2,135
  • 15
  • 24
4

I solved this error by adding a changDetection directive on @component as follows

    @Component({
      selector: 'app-page',
      templateUrl: './page.component.html',
      styleUrls: ['./page.component.scss'],
      changeDetection: ChangeDetectionStrategy.Default
    })

You also need to import it

import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';

There are two strategies onPush and Default

The onPush uses the CheckOnce strategy, meaning that automatic change detection is deactivated until reactivated by setting the strategy to Default (CheckAlways). Change detection can still be explictly invoked.

while the Default uses the CheckAlways strategy, in which change detection is automatic until explicitly deactivated.

Source Docs

Felix Runye
  • 2,135
  • 1
  • 20
  • 20
  • 2
    Weird that you had to specify you wanted to use the *default* value. Is there any place in your configuration where you would define your changeDetectionStrategy as onPush by default ? – Mozgor Oct 18 '18 at 14:27
  • Yes, in some other components, while using angular with firestore, there some instances i had to specify onPush changeDetectionStrategy especially when using local arrays with data that was hardcoded onto a service file. – Felix Runye Oct 19 '18 at 06:27
2

Try creating a deep copy by doing

this.arr = Object.assign({}, NEW_VALUE);
Eddie
  • 761
  • 3
  • 9
  • 22
1

I was able update the component using ngZone

export class comp {
  arr: type[] = []
  
  constructor (
    private zone: NgZone
  ) {}

  updateArr(data: type) {
     this.arr.push(this.zone.run(() => data))
  }

}
Don
  • 71
  • 1
  • 4
0

Incase anyone else runs into my situation, make sure that the component that is updating is the same copy as the one being rendered.

I was using @ViewChild(MyComponent, { static: true }) private test: MyComponent to pass data to the component with the ngfor. (In my case that ended up locking onto another copy I didn't know about)

I was able to fix it by adding the attribute #mycomp to my component tag in the html and changing the above to @ViewChild('mycomp', { static: true }) private test: MyComponent

Tezra
  • 8,463
  • 3
  • 31
  • 68
0

Not a perfect solution but works:

const temp = this.arr;
this.arr = [];
temp.push(obj); // or any other changes
this.arr = temp;

I really don't like importing new stuff because of increasing bundle size and in some cases can cause memory problems so I handle it this way.

Vala Khosravi
  • 2,352
  • 3
  • 22
  • 49
0

If you reassign an array the change detection won't notice the change. So instead of reassigning an array like this.arr = ... change the array directly. This could look like this:

this.arr.splice(0); // empty the array, without reassigning it
this.arr.push(...); // push new items

That way you don't need to call the change detection manually.

Example

Let's say a have a list for a table which can be filtered (the user can i.e. search). So I will have a list which contains all possible items, and a filteredList which is used in the UI and shown to the user.

// in ngOnInit I load the full list
this.list = this.myService.getList();
this.filteredList = [...this.list] // Since I will change the filtered list directly instead of reassigning it, I need to make a copy to not affect the original full list.

// then in a filter function, i.e. filterList()
this.filteredList.splice(0);
this.filteredList.push([...this.list.filter(/* ... */)]); // Again I need to push copies

marcel
  • 2,967
  • 1
  • 16
  • 25
-1

Any time I need to deal with *ngFor that needs change detection I prefer using behaviorSubject and async pipe. (I know this is a longer solution. Using behavior subject makes it easy to work with change detection)

array$ = new BehaviorSubject([]);//Declare your array

When you need to update your array

array$.next(newArray)://Pass in new array data

if you want to read your array value in the .ts file use this

array$.getValue()

in your .HTML file

*ngFor='let obj of (array$ | async)'