104

I have a parent component that retrieves an array of objects using an ajax request.

This component has two children components: One of them shows the objects in a tree structure and the other one renders its content in a table format. The parent passes the array to their children through an @input property and they display the content properly. Everything as expected.

The problem occurs when you change some field within the objects: the child components are not notified of those changes. Changes are only triggered if you manually reassign the array to its variable.

I'm used to working with Knockout JS and I need to get an effect similar to that of observableArrays.

I've read something about DoCheck but I'm not sure how it works.

Mel
  • 5,837
  • 10
  • 37
  • 42
pablolmedorado
  • 1,181
  • 2
  • 8
  • 11

8 Answers8

135

OnChanges Lifecycle Hook will trigger only when input property's instance changes.

If you want to check whether an element inside the input array has been added, moved or removed, you can use IterableDiffers inside the DoCheck Lifecycle Hook as follows:

constructor(private iterableDiffers: IterableDiffers) {
    this.iterableDiffer = iterableDiffers.find([]).create(null);
}

ngDoCheck() {
    let changes = this.iterableDiffer.diff(this.inputArray);
    if (changes) {
        console.log('Changes detected!');
    }
}

If you need to detect changes in objects inside an array, you will need to iterate through all elements, and apply KeyValueDiffers for each element. (You can do this in parallel with previous check).

Visit this post for more information: Detect changes in objects inside array in Angular2

seidme
  • 12,543
  • 5
  • 36
  • 40
35

You can always create a new reference to the array by merging it with an empty array:

this.yourArray = [{...}, {...}, {...}];
this.yourArray[0].yourModifiedField = "whatever";

this.yourArray = [].concat(this.yourArray);

The code above will change the array reference and it will trigger the OnChanges mechanism in children components.

Wojtek Majerski
  • 3,489
  • 2
  • 16
  • 9
  • This worked pretty well for me in my situation. – Jared Christensen May 08 '19 at 18:49
  • how is this from the point of view of performance and memory consumption? – Ernesto Alfonso Jan 30 '20 at 11:10
  • 1
    @ErnestoAlfonso: it's a zero performance operation because you do not clone the array content (elements). You only create a new variable that points to the same memory area as the previous one. – Wojtek Majerski Feb 25 '20 at 11:18
  • your answer saved my day – stringnome Oct 20 '20 at 21:16
  • i love this answer, cause it work in react\vue\angular; – zhouwangsheng Mar 13 '21 at 02:18
  • 4
    @WojtekMajerski: "You only create a new variable that points to the same memory area as the previous one" - it cannot point to the same area (that's why they are considered different in the first place), `Array.concat` always creates a new instance, so all references to elements of the initial array are copied (shallow copy). Once you do `var x = [].concat(y)`, you can separately push to both `x` and `y`, they are independent. If you aren't using `y` afterwards, if will be collected by the GC. – vgru Jun 30 '21 at 14:45
11

Read following article, don't miss mutable vs immutable objects.

Key issue is that you mutate array elements, while array reference stays the same. And Angular2 change detection checks only array reference to detect changes. After you understand concept of immutable objects you would understand why you have an issue and how to solve it.

I use redux store in one of my projects to avoid this kind of issues.

https://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.html

Jan Cejka
  • 321
  • 1
  • 4
10

You can use IterableDiffers

It's used by *ngFor

constructor(private _differs: IterableDiffers) {}

ngOnChanges(changes: SimpleChanges): void {
  if (!this._differ && value) {
     this._differ = this._differs.find(value).create(this.ngForTrackBy);
  }
}

ngDoCheck(): void {
  if (this._differ) {
    const changes = this._differ.diff(this.ngForOf);
    if (changes) this._applyChanges(changes);
  }
}
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
6

It's work for me:

@Component({
  selector: 'my-component',
  templateUrl: './my-component.component.html',
  styleUrls: ['./my-component.component.scss']
})
export class MyComponent implements DoCheck {

  @Input() changeArray: MyClassArray[]= [];
  private differ: IterableDiffers;

  constructor(private differs: IterableDiffers) {
    this.differ = differs;
  }

  ngDoCheck() {
    const changes = this.differ.find(this.insertedTasks);
    if (changes) {
      this.myMethodAfterChange();
  }
}
Lu4
  • 14,873
  • 15
  • 79
  • 132
Stack Over
  • 287
  • 2
  • 4
  • 14
  • I am marking you up for this, it helped me more than any of the others. Not sure why you were marked with a minus! – r3plica Nov 18 '18 at 14:20
  • 2
    As far as I see it, this is not a solution as `changes` is always set - which means you could just do `this.myMethodAfterChange();` inside `ngDoCheck`. This would work but create a performance issue. – NoRyb Feb 20 '19 at 14:01
3

This already appears answered. However for future problem seekers, I wanted to add something missed when I was researching and debugging a change detection problem I had. Now, my issue was a little isolated, and admittedly a stupid mistake on my end, but nonetheless relevant. When you are updating the values in the Array or Object in reference, ensure that you are in the correct scope. I set myself into a trap by using setInterval(myService.function, 1000), where myService.function() would update the values of a public array, I used outside the service. This never actually updated the array, as the binding was off, and the correct usage should have been setInterval(myService.function.bind(this), 1000). I wasted my time trying change detection hacks, when it was a silly/simple blunder. Eliminate scope as a culprit before trying change detection solutions; it might save you some time.

bess
  • 39
  • 1
3

Instead of triggering change detection via concat method, it might be more elegant to use ES6 destructuring operator:

this.yourArray[0].yourModifiedField = "whatever";
this.yourArray = [...this.yourArray];
fonzane
  • 368
  • 1
  • 4
  • 16
1

You can use an impure pipe if you are directly using the array in your components template. (This example is for simple arrays that don't need deep checking)

@Pipe({
  name: 'arrayChangeDetector',
  pure: false
})
export class ArrayChangeDetectorPipe implements PipeTransform {
  private differ: IterableDiffer<any>;

  constructor(iDiff: IterableDiffers) {
    this.differ = iDiff.find([]).create();
  }

  transform(value: any[]): any[] {
    if (this.differ.diff(value)) {
      return [...value];
    }
    return value;
  }
}
<cmp [items]="arrayInput | arrayChangeDetector"></cmp>

For those time travelers among us still hitting array problems here is a reproduction of the issue along with several possible solutions.

https://stackblitz.com/edit/array-value-changes-not-detected-ang-8

Solutions include:

  • NgDoCheck
  • Using a Pipe
  • Using Immutable JS NPM github
Rheimus
  • 47
  • 4