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

Most upvoted comments

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.

  1. Basically, this problem mostly appears when using the async pipe.
  2. Most of the people seeing it, also often use the pipe with an observable coming from ngrx
  3. Because of the way ngrx is used, it can happen than even if something changes in the store, a select query would produce an identical result.
  4. Usually people exploits distinctUntilChanged to avoid that a new value is emitted.
  5. Unfortunately, when the store starts to get complex and the values produced involve nested structures with arrays and objects, it becomes impossible to not produce a new value, even though it is semantically identical.
  6. The devModeEquals method does a deep comparison, so we expect from it not to complain in that case.
  7. But what we forgot, is that the async pipe uses wrapped values to explicitly specify that a value has changed: the async pipe is generic and from the point of view of an observable, a new value emitted is an important event, even if the value is semantically the same.
  8. Hence, it is normal for angular to complain that something changed after it has already been checked, because something DID change, an event was emitted by the observable, and as said before, this is a meaningful event in most uses of observables.

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 an async pipe to initialize a child component.

<!-- items$: Observable<Item[]> -->
<app-child-component [values]="items$ | async"></app-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:

  1. Parent: OnInit
  2. Parent: items$ emits a value that is piped to async, pipe output is bound to Child
  3. Child: OnInit
  4. Parent: items$ emits a value that is piped to async, pipe output is bound to Child
  5. Child: AfterViewInit
  6. Parent: AfterViewInit
  7. Parent: ExpressionChangedAfterItHasBeenCheckedError

I think the 4th step is the breaking step because of the timing (between OnInit and AfterViewInit).

image

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 because checkBindingNoChanges 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 the checkBindingNoChanges 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

import { Component } from '@angular/core';
import { Store } from '@ngrx/store';

@Component({
  selector: 'app-root',
  template: `
    <div>
      Root counter: {{ counter$ | async }}

      <span *ngIf="counter$ | async">Not 0</span>
    </div>

    <counter></counter>
  `
})
export class AppComponent {
  counter$ = this.store.select(state => state.counter);

  constructor(
    private store: Store<any>
  ) { }
}

counter.component.ts

import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';

@Component({
  selector: 'counter',
  template: `
    Counter {{ counter$ | async }}

    <button type="button" (click)="onIncrement()">+</button>
    <button type="button" (click)="onDecrement()">-</button>
  `
})
export class CounterComponent implements OnInit {
  counter$ = this.store.select(state => state.counter);

  constructor(
    private store: Store<any>
  ) { }

  onIncrement() {
    this.store.dispatch({
      type: 'INCREMENT'
    });
  }

  onDecrement() {
    this.store.dispatch({
      type: 'DECREMENT'
    });
  }

  ngOnInit() {
    this.store.dispatch({
      type: 'INCREMENT'
    });
  }
}

The cause of the error is the dispatch in the ngOnInit of the CounterComponent. Wrapping it in a setTimeout “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 function unwrapValue at the line 38 which states view.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 with

view.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 (in change_detection_util.ts) fixes this problem:

    a = (a && a.wrapped) ? a.wrapped : a;
    b = (b && b.wrapped) ? b.wrapped : b;