Clarify docs or improvement to avoid ExpressionChangedAfterItHasBeenCheckedError
See original GitHub issueI’m submitting a…
[ ] Regression (a behavior that used to work and stopped working in a new release)
[ ] Bug report
[X] Feature request
[X] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question
Current behavior
As evident from numerous stack overflow threads, blog articles and issues here, people stumble upon the ExpressionChangedAfterItHasBeenCheckedError
time and again. In some cases, people just make mistakes. Other common behaviors include something like
- Child (or later sibling) gets its first onChanges and emits an output.
- Parent changes state
NOTE: This case also fails in these cases
{{child.childOutput | async}}
<child #child>
Expected behavior
I think everyone would benefit from a clear guidance on the recommended way of treating this. If needed, the documentation should then also be updated. Btw, right now I don’t find much at all on @Output
.
What is the motivation / use case for changing the behavior?
So how should it work then? Some suggestions.
- Just use
new EventEmitter(true)
for your outputs where this could happen. This really needs to be made clear then in the documentation. And the bad thing with this approach is that it doesn’t work forval | async
bindings - a case where this is a problem as well. And there is potentially a benefit with not using e.g..debounceTime(0)
globally on the Observable. Sure, in these cases the client could have its own observable with.debounceTime(0)
, but it quickly becomes messy with many cryptic solutions. - Manually handle this in the parent component by calling
cdRef.markForCheck()
orcdRef.detectChanges()
. IMO cumbersome. And…which one should I use and why? And is this even enough always. Doesn’t the async pipe already do this? Or should I runZone.current.scheduleMicroTask('', () => cdRef.markForCheck())
? Again, document the whats and whys? - Improve Angular to automatically call e.g.
cdRef.markForCheck()
(or via microtask pattern above) when an event is handled through a template binding in the “Detect changes in children” phase. Isn’t this already done btw? Is doing it via a microtask key here?
Number 3. seems to be the easiest on clients and least error prone, that would be my choice. Of the cons I could imagine is “How do you detect circular changes?”. That’s a hard one. But, we’re not solving it by urging everyone do do new EventEmitter(true)
. That, to me, just looks like an anti-pattern to hide such errors behind asynchronous bars. My suggestion for that would be to do something similar to Angular 1, and keep a flag, essentially doing something like cdRef.markForCheckButDoneAsResultOfChildOutput()
- and if appRef.tick
s are run with that flag more than, say 10 times, yield an error. Alternatively check which bindings were marked and not let the same change detector ref be marked twice in the same macro task.
Environment
Angular version: 4.4.6
Issue Analytics
- State:
- Created 6 years ago
- Reactions:2
- Comments:6 (3 by maintainers)
Top GitHub Comments
@tytskyi - thanks for your reply
right
Documentation
First, I think this is a difficult and big enough knowledge domain to require its own page in the documentation. Something like “Data flows” or “Inputs/Outputs”. One paragraph there could be specifically about this error, which would be linked from the actual exception message in the console. Thus, users would find it. At least those who stumble upon it.
Secondly, users looking in documentation need to know how to solve their problems. Not primarily what not to do. In other words, what are some common scenarios where you’d face this problem, and how would you solve them. Let me give you an example:
Concrete example
In the HeaderComponent there’s a button that opens up a WelcomeGuideComponent. The WelcomeGuideComponent can also be opened by other means, e.g. an internal keyboard shortcut listener that it sets up. Or via query param changes. The HeaderComponent is not responsible for giving it its initial value. Whenever it’s open, the HeaderComponent button should have a special css class. Naturally I think most people would try something like
…but if WelcomeGuide in its
ngOnInit
checks queryParams, and emits an initialopenChange.emit(true)
, this breaks. I disagree that this would be very rare. On the contrary, my impression (and I think the results on stack overflow/google support this) is quite common. So, telling people that “the component should not update parent’s state during the component’s lifecycle hook synchronously” is, albeit correct, not very helpful. How should the user solve it then? It seems to me, the general consensus now (as recommended in other sources) is to usenew EventEmitter(true)
foropenChange
.For discussion’s sake, let me broaden this a bit. Say we have multiple WelcomeGuide components but only one should be visible at a time. Thus we have a little
WelcomeGuideSvc
, which keeps track of the active (if any) and broadcasts events about the active one to allWelcomeGuide
components. EveryWelcomeGuide
component is responsible to listen on these events and close if needed. Now, should I use thesetTimeout(() => welcomeGuideSvc.setActive(this))
or justwelcomeGuideSvc.setActive(this)
? Where lies the responsibility? Maybe we say that as a rule you must not have any side effects outside of your component as a synchronous result of ngOnChanges. That sounds complicated, but you realize asetTimeout
does the job. That’s fine, but let’s assume now there’s state to indicate that severalWelcomeGuide
s should open initially. They all see thatWelcomeGuideSvc.active === null
and display. As our event is asynchronous we see un unfortunate flicker at first render since they appear in the DOM only to be closed (all but one) on next tick. So you’re damned if you do, and damned if you don’t.Moving on to another point…let’s get back to the original case, what if you make the EventEmitter asynchronous. And then do (bad code)
Now this may seem contrived, but I’ve seen similar cases in real life. This error is very hard to debug. The browser will likely just freeze. If we supported synchronous events, there would be a possibility to build a detector (Angular 1 style) and throw an error.
Then we get back to the various solutions I outlined in my initial post with some pros and cons. But met le reiterate.
setTimeout
,new EventEmitter(true)
etc. when appropriate + solves most problems - complex to understand for Angular users - not one easy single solution - there are scenarios that are not solved - can result in undetected asynchronous infinite loopsI miss valid cons with the second one.
This is a really great writeup! Thanks for the input @staeke. I’m going to close this and roll it into #33731.