angular: checkBindingNoChanges does not get unwrapped old values when checking for ExpressionChangedAfterItHasBeenCheckedError
I’m submitting a…
[ ] Regression (a behavior that used to work and stopped working in a new release)
[x] Bug report
[ ] Feature request
[ ] 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
When using impure pipes, the value is wrapped, for example with async
.
Sometimes this makes the developer mode of Angular throw the error ExpressionChangedAfterItHasBeenCheckedError
because checkBindingNoChanges
gets a wrapped value as the old value and an unwrapped value as the new value even though they did not change (I simply looked at the values via the chromium debugger)
Expected behavior
Either checkBindingNoChanges
should unwrap values if needed or get unwrapped values from its caller.
Minimal reproduction of the problem with instructions
Couldn’t reproduce it: I think it happens in some complex scenario where the order of initialisation of the component and the moment data is received via an observable is not usual.
Note that my observables are initialized in ngOnInit
and simply bound in the template via obs$ | async
without anything else specific. I don’t see how it could be simpler than that, none of the usual ways of fixing ExpressionChangedAfterItHasBeenCheckedError
can even be applied!
I guess the problem existed before but since 4.2.x the checks are stricter and now the problem shows itself.
Environment
Angular version: 4.2.6
Browser:
- [x] Chrome (desktop) version XX
- [ ] Chrome (Android) version XX
- [ ] Chrome (iOS) version XX
- [ ] Firefox version XX
- [ ] Safari (desktop) version XX
- [ ] Safari (iOS) version XX
- [ ] IE version XX
- [ ] Edge version XX
For Tooling issues:
- Node version: XX
- Platform:
Others:
About this issue
- Original URL
- State: closed
- Created 7 years ago
- Reactions: 20
- Comments: 23 (15 by maintainers)
Commits related to this issue
- fix(core): not throw when old value is wrapped while new value is same unwrapped one closes #15721, closes #18129 — committed to trotyl/angular by trotyl 6 years ago
- fix(core): not throw when old value is wrapped while new value is same unwrapped one closes #15721, closes #18129 — committed to trotyl/angular by trotyl 6 years ago
- fix(core): not throw when old value is wrapped while new value is same unwrapped one closes #18129 — committed to trotyl/angular by trotyl 6 years ago
- fix(core): not throw when old value is wrapped while new value is same unwrapped one closes #18129 — committed to trotyl/angular by trotyl 6 years ago
Here is a counter-argument to this issue, mostly thinking out loud, but it may explain the situation and why this is not a bug.
distinctUntilChanged
to avoid that a new value is emitted.devModeEquals
method does a deep comparison, so we expect from it not to complain in that case.The solution would thus be to refactor your store in order not to emit new values when nothing change. Or to rewrite your selectors with the same goal in mind.
Unfortunately, it is not easy and sometimes very cumbersome!
The problem is thus not a bug, but a question of design and if the use of ngrx, the async pipe, etc are justified and/or practicable!
@tbosch, @alxhub or @robwormald, it would be great if you could at least provide some feedbacks about this reasoning and if it is justified, we can close this issue (and then starts complaining about the inability of angular and ngrx to be combined together in complex scenario 😛).
I think ran into this bug today. Sorry, no simple repro, but I will describe my app.
I have an
Observable<Item[]
> that emits values from my ngrx store. I use it in a template along with anasync
pipe to initialize a child component.The items Observable emits twice when the parent component is initialized. I don’t know exactly why it emits twice, but the timing of the second emission seems to be the problem.
Order of events in my Parent and Child components:
items$
emits a value that is piped toasync
, pipe output is bound to Childitems$
emits a value that is piped toasync
, pipe output is bound to ChildI think the 4th step is the breaking step because of the timing (between OnInit and AfterViewInit).
I tried to step through
core.es5.js
but didn’t understand much of what it does. From what I can tell, I’m seeing this error becausecheckBindingNoChanges
does not unwrap values.I’m having the same issues here with ngrx. I’m loading data in the
ngOnInit
of my child component, one of the results of the HTTP response is being shown in the parent component which results in thecheckBindingNoChanges
error. After looking at the examples and reading @robwormald his explanation here it makes sense.A simple representation of the problem looks like this
app.component.ts
counter.component.ts
The cause of the error is the dispatch in the
ngOnInit
of theCounterComponent
. Wrapping it in asetTimeout
“works” but feels really hacky and looks like it defeats the purpose of using a Store.So somehow I totally understand why it fails, because the check of the
app.component
has already passed and we’re changing data which results in an update in the view. But on the other hand, it feels really weird when using ngrx because it’s the single-source of truth. If the data in the store updates, it will result in an update in the view.I’ve bean experimenting with plunker mentioned in a comment of related issue.
I think that a problem might be in
core/src/view/util.ts
in a functionunwrapValue
at the line 38 which statesview.oldValues[globalBindingIdx] = new WrappedValue(oldValue);
Since
unwrapValue
unwraps the current value, I believe that old value should also be stored as unwrapped variant instead as wrapped one. This can be achieved by replacing the line above withview.oldValues[globalBindingIdx] = oldValue;
With this change, mentioned plunker works correctly and change makes sense to me.
Has this bug been confirmed by anyone on the angular team. It is hitting us hard right now.
Does anyone have a monkey patch we can do to workaround it without having to use a modified build of angular?
For the record, simply adding the following to
devModeEqual
(inchange_detection_util.ts
) fixes this problem: