When subscribing to a QueryList change events and when reacting to the event and changing a property that the view is bound to. I get a ExpressionChangedAfterItHasBeenCheckedError and the value is like the old value.
I made a stackblitz example show casing the error.
I can solve it by adding a microtask to the queue. But I'm just trying to understand what is happening.
Thanks
here is what happens;
in dev mode, two consecutive CD cycles happen as explained in Relevant change detection operations section of this article;
A running Angular application is a tree of components. During change detection Angular performs checks for each component which consists of the following operations performed in the specified order:
- update bound properties for all child components/directives
- call ngOnInit, OnChanges and ngDoCheck lifecycle hooks on all child components/directives
- update DOM for the current component
- run change detection for a child component
- call ngAfterViewInit lifecycle hook for all child components/directives
comp
has 1 element comp
added to DOM and {{ count.length }}
is projected to DOM as 0@ViewChildren('comp') test: QueryList<ElementRef>
is updated just before step 5. test: QueryList
has 1 elementQueryList.changes
observable emits at the same time when ViewChildren
query is set, just before step 5. this.count.push(this.test.length)
gets executed and count.length
becomes 1.ngAfterViewChecked
is executed and first CD (relevant part of our investigation) ends {{ count.length }}
was projected to DOM as 0 but now count.length
is 1. It throws exception at this point.To explain more; changing {{ count.length }}
to {{ count }}
doesn't throw error because object reference didn't change.
Similarly; changing {{ count.length }}
to {{ comps.length }}
also doesn't throw error because comps.length
was also 1 before 1st CD cycle begins.
I also created a demo that prints relevant steps during component lifecycle related to our investigation. You can see that exception is thrown before 4th CD cycle begins. (1st and 2nd CD cycles happen before button click so in demo 3rd and 4th CD cycles should be observed). Also pay attention to the point where QueryList.changes
emits value.
https://stackblitz.com/edit/angular-tmcttm
Finally, as you said, the solution to this particular problem is to add a microtask to the queue by changing this line
this.count.push(this.test.length);
into this
setTimeout(_ => this.count.push(this.test.length));