I made a very basic form. During submission the button is disabled and once the work is done it should be enabled.
However this doesn't work, instead I get the infamous ExpressionChangedAfterItHasBeenCheckedError
.
I read Max Koretskyi articles about CD and this exception but can't understand why this happens in this case? It is a simple reaction to an event. Could someone explain in detail what exactly causes this and how could I fix it without setTimeout
or Promise
?
Here is the sample code:
https://stackblitz.com/edit/angular-change-detection-err?file=src/app/app.component.ts
I fork your Stack-Blitz and make the error disappear by triggering detectChanges()
on your finalize
.
Stack-Blitz with detectChanges
I will try to explain why this is happening. The error seems to be trigger on finalize
because it runs after the async pipe
, when the Observable
completed.
In other words angular trigger the detection check (as always does after a async
operation if your component is not OnPush
) but after the detection function is finished the values in the properties checked are not the same and that's the reason that you get that error. If you trigger manual the change detection on finilize
Angular will check for changes again and everything will be OK.
(Edited on 2020-09-14)
Another solution is to add a delay(0)
to your pipeline before finalize
or tap
functions. more details here: Angular-Debugging.
And that's the sample code of our example: Stack-Blitz with delay
(Edited on 2019-09-16)
Debugged the code for a while and now it is clear what happens.
Event handler update()
runs on submit, sets submissionInProgress
to true
, and sets the submissionResult$
observable .
Change detection kicks in after the event. It evalutes other bindings if there are (in the order of your template) then the submissionInProgress
binding, records its value as true. The next binding is the async pipe
for submissionInProgress$
. The async pipe
notices that its input is not null anymore and subscribes to it. However of(true)
results a sync observable hence during subscription the observable evalutes. OnNext emits, asyn pipe's _updateLatestValue
method sets its _latestValue
field, the observable completes and the finalize
method runs. Finalize method updates submissionInProgress
field to false
. Notice that angular already passed the submissionInProgress
binding and recorded its value as true, but we just updated the field by finalize
.
CD records that the asyn pipe
binding has a value true now.
After these the 2nd round of CD kicks in where it checks for no changes.
It evaluates the submissionInProgress
binding and sees that the value was true but now it false so it throws the exception.
If we add a delay
or setTimeout
, subscription will happen but no business logic will run, the code will remain in the task queue?
. Stack empties, task queue is processed, the modell will be updated with the emitted value and the finalize's side effect submissionInProgress = false
. After these will kick in the CD and that's fine now.
Useful points for debugging:
view.detectChanges();
, view.checkNoChanges();
, function bindingUpdated(lView, bindingIndex, value)
*ngIf="async..."
class AsyncPipe {
functions