UPDATED: Angular 6 + Material: Child component with simple and mat-accordion lists is updated seemingly by holding reference to parent array
UPDATED: Angular 6 + Material: Child component with simple and mat-accordion lists is updated seemingly by holding reference to parent array
(UPDATE: Issue exists when child displays simple mat-accordion list and a simple list. I've juxtaposed both simple <div>
and accordion lists to show possible change detection effects in simple list and mat-accordion. It seems child is holding reference to parent array of 'things'.)
<div>
Angular 6+. Material 6+ Simple code and image below. I've cut this down to as short as possible. Also note ChangeDetectionStrategy.OnPush is active for both child and parent.
(Stackblitz at: https://stackblitz.com/edit/angular-child-updated)
SHOWING LIST OF 'THINGS' (Via Child's @Input 'things')
I'm doing something very simple: showing an array of strings (things$
) passed to child component (ChildComponent
) which contains both a simple <div>
list and a Material <mat-accordion>
list (for this post's comparison purposes). The data for the lists is passed by the parent (app.component.ts
) via an async | pipe
@Input property to the child. The parent obtains the things$
observable by making a faked 'http' call fakeHttpGetThings()
which returns a mocked list of 'things' as an observable. NOTE: Parent redundantly holds an array of things in this.thingsList
! All very nice.
things$
ChildComponent
<div>
<mat-accordion>
app.component.ts
async | pipe
things$
fakeHttpGetThings()
this.thingsList
ADDING A 'THING' TO PARENT'S ARRAY LIST BUT CHILD's <mat-accordion>
IS UPDATED ON CLICK EVENTS ANYWAY...
<mat-accordion>
As mentioned, the parent is also maintaining an array of 'things' in this.thingsList
where it is initially assigned via an rxjs map()
when the parent makes the fakeHttpGetThings()
to get all 'things'. It is also updated upon addThing()
. Redundant, but so what. BUT, the parent also wants to ADD a 'thing' to the child lists (incorrectly but it still somewhat works). A faked 'http' fakeHttpAddThing('new thing')
call is made, which returns the 'thing' to be added. It is redundantly added the parent's this.thingsList
as well. NOTE, here the simple <div>
and <mat-acccordion>
lists in the Child were never directly manipulated, i.e., there is no dedicated @Input for the new 'thing' to be added to the child. On return of fakeHttpAddThing()
, within the subscribe( res ...)
, the new 'thing'
is merely pushed to the this.thingsList
array IN THE PARENT app.component.ts.
this.thingsList
map()
fakeHttpGetThings()
addThing()
fakeHttpAddThing('new thing')
this.thingsList
<div>
<mat-acccordion>
fakeHttpAddThing()
subscribe( res ...)
'thing'
this.thingsList
Why?: Does adding to parent array update the child's simple <div>
and lists even with ChangeDetectionStrategy.OnPush which is active all over?
<div>
ISSUE: BUT the simple <div>
and <mat-accordion>
lists do get updated when one of the accordion panels is expanded (events are triggered). This happens even though the 'irrelevant' this.thingsList
in the parent is updated and ChangeDetectionStrategy.OnPush is active for both parent and child. Why?
<div>
<mat-accordion>
this.thingsList
Summary
Again, I had passed the original list of things via '@Input set' in child.component.ts
. But upon add, even though the list array in the child is never directly updated (the @Input set is NOT triggered), the child's simple list and the list of things shows the new thing added upon panel expansion. Why? I'm stumped. This shouldn't be working IMO. It seems the child is holding a reference to the parent's this.thingsList
array. Any thoughts will be greatly appreciated. Snippets below:
child.component.ts
this.thingsList
app.component.ts:
import Observable, of from 'rxjs';
import map from 'rxjs/operators';
import Component, OnInit, ChangeDetectionStrategy from '@angular/core';
@Component( async)?.things">
</app-child>
<input
#thingbox>
<button (click)="addThing(thingbox.value)">add thing</button>
<button>do nothing</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush
)
export class AppComponent implements OnInit
thingsList: any;
things$: Observable<message: string, things: any>;
constructor()
addThing = (thing: string): void =>
this.fakeHttpAddThing(thing)
.pipe(
map( (res) =>
return res;
),
)
.subscribe( (res) =>
// I KNOW THIS map() ASSIGNMENT IS WRONG AND UNNECCESARY
// BUT STILL CHANGE DETECTION IS TRIGGERED IN ChildComponent
// EVERYTHING WORKS FINE IF REMOVED.
this.thingsList.push(res.thing);
return res;
);
ngOnInit()
this.things$ = this.fakeHttpGetThings()
.pipe(
map( (thingsResponse): message: string, things: any =>
// I KNOW THIS map() ASSIGNMENT IS WRONG AND UNNECCESARY
// BUT STILL CHANGE DETECTION IS TRIGGERED IN ChildComponent
// EVERYTHING WORKS FINE IF REMOVED.
this.thingsList = thingsResponse.things;
return thingsResponse;
),
);
fakeHttpGetThings = (): Observable<message: string, things: any> =>
const things: any = [
thingKey: 'THING1',
,
thingKey: 'THING2',
,
thingKey: 'THING3',
];
return of(message: 'SUCCESS', things: things);
fakeHttpAddThing(thing: string): Observable<message: string, thing: any>
return of(
message: 'SUCCESS',
thing: thingKey: thing
);
child.component.ts
import Component, OnInit, Input, ChangeDetectionStrategy from '@angular/core';
@Component(
selector: 'app-child',
template:
`
<div
*ngFor="let thing of _things">
thing.thingKey
</div>
<mat-accordion>
<mat-expansion-panel
*ngFor="let thing of _things">
<mat-expansion-panel-header>
thing.thingKey
</mat-expansion-panel-header>
<p>thing.thingKey contents</p>
</mat-expansion-panel>
</mat-accordion>
`,
changeDetection: ChangeDetectionStrategy.OnPush
)
export class ChildComponent implements OnInit
_things: any = null;
@Input()
set things(things)
this._things = things;
constructor()
ngOnInit()
Thanks. Added SB link.
– MoMo
Aug 23 at 18:36
1 Answer
1
You're not using OnPush change detection so change detection is firing in every component basically every time a user interacts with your app (clicking, hovering, scrolling etc) and that things array is always the same reference being manipulated no matter where you do it.
if you want to use on push change detection then do:
import ChangeDetectionStrategy from '@angular/core';
@Component(
selector: 'app-child',
template:
`
<div
*ngFor="let thing of _things">
thing.thingKey
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
)
Now change detection won't trigger unless some change detection trigger occurs (input/output events, async pipe events etc)
how is the child gaining access to the array reference?
– MoMo
Aug 23 at 18:38
arrays are passed by reference in javascript, all your components / functions are showing / manipulating / passing the same array.
– bryan60
Aug 23 at 18:39
You're referring to this.thingsList = thingsResponse.things. The child is using that reference since it's passed implicitly to it by the response async'd Observable.....
– MoMo
Aug 23 at 18:42
you only ever create one array and pass the reference around. Any consumer that manipulates that array is manipulating the reference so any consumer holding that reference will see the changes.
– bryan60
Aug 23 at 18:43
you do explicitly pass the array to the child at [things]="(things$ | async)?.things", the array coming through the async pipe is the exact same array reference that you save a reference to on your component in the map() function in our onInit hook, and the same reference that you later manipulate in your add thing function. Arrays references are always what is passed, so you are passing it explicitly. Same for objects. Primitives (strings, numbers, booleans) are different, they're passed by value.
– bryan60
Aug 23 at 18:56
By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.
Can you please create a StackBlitz project instead. stackblitz.com
– Siddharth Ajmera
Aug 23 at 18:22