Angular2 change detection: ngOnChanges not firing for nested object

Mark Rajcok

rawLapsData continues to point to the same array, even if you modify the contents of the array (e.g., add items, remove items, change an item).

During change detection, when Angular checks components' input properties for change, it uses (essentially) === for dirty checking. For arrays, this means the array references (only) are dirty checked. Since the rawLapsData array reference isn't changing, ngOnChanges() will not be called.

I can think of two possible solutions:

  1. Implement ngDoCheck() and perform your own change detection logic to determine if the array contents have changed. (The Lifecycle Hooks doc has an example.)

  2. Assign a new array to rawLapsData whenever you make any changes to the array contents. Then ngOnChanges() will be called because the array (reference) will appear as a change.

In your answer, you came up with another solution.

Repeating some comments here on the OP:

I still don't see how laps can pick up on the change (surely it must be using something equivalent to ngOnChanges() itself?) while map can't.

  • In the laps component your code/template loops over each entry in the lapsData array, and displays the contents, so there are Angular bindings on each piece of data that is displayed.
  • Even when Angular doesn't detect any changes to a component's input properties (using === checking), it still (by default) dirty checks all of the template bindings. When any of those change, Angular will update the DOM. That's what you are seeing.
  • The maps component likely doesn't have any bindings in its template to its lapsData input property, right? That would explain the difference.

Note that lapsData in both components and rawLapsData in the parent component all point to the same/one array. So even though Angular doesn't notice any (reference) changes to the lapsData input properties, the components "get"/see any array contents changes because they all share/reference that one array. We don't need Angular to propagate these changes, like we would with a primitive type (string, number, boolean). But with a primitive type, any change to the value would always trigger ngOnChanges() – which is something you exploit in your answer/solution.

As you probably figured out by now object input properties have the same behavior as array input properties.

Not the cleanest approach, but you can just clone the object each time you change the value?

   rawLapsData = Object.assign({}, rawLapsData);

I think I would prefer this approach over implementing your own ngDoCheck() but maybe someone like @GünterZöchbauer could chime in.

As an extension to Mark Rajcok's second solution

Assign a new array to rawLapsData whenever you make any changes to the array contents. Then ngOnChanges() will be called because the array (reference) will appear as a change

you can clone the contents of the array like this:

rawLapsData = rawLapsData.slice(0);

I am mentioning this because

rawLapsData = Object.assign({}, rawLapsData);

didn't work for me. I hope this helps.

If the data comes from an external library you might need to run the data upate statement within zone.run(...). Inject zone: NgZone for this. If you can run the instantiation of the external library within zone.run() already, then you might not need zone.run() later.

Tim Phillips

Use ChangeDetectorRef.detectChanges() to tell Angular to run a change detection when you edit a nested object (that it misses with its dirty checking).

Danish Dullu

In Case of Arrays you can do it like this:

In .ts file (Parent component) where you are updating your rawLapsData do it like this:

rawLapsData = somevalue; // change detection will not happen

Solution:

rawLapsData = {...somevalue}; //change detection will happen

and ngOnChanges will called in child component

Simon H

My 'hack' solution is

   <div class="col-sm-5">
        <laps
            [lapsData]="rawLapsData"
            [selectedTps]="selectedTps"
            (lapsHandler)="lapsHandler($event)">
        </laps>
    </div>
    <map
        [lapsData]="rawLapsData"
        [selectedTps]="selectedTps"   // <--------
        class="col-sm-7">
    </map>

selectedTps changes at the same time as rawLapsData and that gives map another chance to detect the change through a simpler object primitive type. It is NOT elegant, but it works.

Here's a hack that just got me out of trouble with this one.

So a similar scenario to the OP - I've got a nested Angular component that needs data passed down to it, but the input points to an array, and as mentioned above, Angular doesn't see a change as it does not examine the contents of the array.

So to fix it I convert the array to a string for Angular to detect a change, and then in the nested component I split(',') the string back to an array and its happy days again.

Change detection is not triggered when you change a property of an object (including nested object). One solution would be to reassign a new object reference using 'lodash' clone() function.

import * as _ from 'lodash';

this.foo = _.clone(this.foo);

I have 2 solutions to resolve your problem

  1. Use ngDoCheck to detect object data changed or not
  2. Assign object to a new memory address by object = Object.create(object) from parent component.

I stumbled upon the same need. And I read a lot on this so, here is my copper on the subject.

If you want your change detection on push, then you would have it when you change a value of an object inside right ? And you also would have it if somehow, you remove objects.

As already said, use of changeDetectionStrategy.onPush

Say you have this component you made, with changeDetectionStrategy.onPush:

<component [collection]="myCollection"></component>

Then you'd push an item and trigger the change detection :

myCollection.push(anItem);
refresh();

or you'd remove an item and trigger the change detection :

myCollection.splice(0,1);
refresh();

or you'd change an attrbibute value for an item and trigger the change detection :

myCollection[5].attribute = 'new value';
refresh();

Content of refresh :

refresh() : void {
    this.myCollection = this.myCollection.slice();
}

The slice method returns the exact same Array, and the [ = ] sign make a new reference to it, triggering the change detection every time you need it. Easy and readable :)

Regards,

Yazan Khalaileh

I have tried all the solutions mentioned here, but for some reason ngOnChanges() still did not fire for me. So I called it with this.ngOnChanges() after calling the service that repopulates my arrays and it worked .... correct? probably not. Neat? hell no. Works? yes!

ok so my solution for this was:

this.arrayWeNeed.DoWhatWeNeedWithThisArray();
const tempArray = [...arrayWeNeed];
this.arrayWeNeed = [];
this.arrayWeNeed = tempArray;

And this trigger me ngOnChanges