angular / angular

Deliver web apps with confidence 🚀
https://angular.dev
MIT License
94.97k stars 24.84k forks source link

ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked on ng 4 #17572

Closed altreze closed 6 years ago

altreze commented 7 years ago

I'm submitting a ...


[ ] Regression (behavior that used to work and stopped working in a new release)
[X ] Bug report #14748 
[ ] 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

After using an observable object in my template using async, I am receiving : ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: ''. Current value: 'something'

Expected behavior

Please tell us about your environment


Angular version: 4.2.2

Browser:
- [X] Chrome (desktop) version Version 58.0.3029.110 (64-bit)

For Tooling issues:
- Node version: v6.10.3
- Platform: Mac            
Bigless27 commented 7 years ago

Had this same issue with using redux. Here is a link to issue I posted on their repository. When on angular 2 there are no errors but on angular 4.2.2 I get expression changed after it was checked errors. https://github.com/angular-redux/store/issues/428

mpalourdio commented 7 years ago

The problem appears for me on my components when bumping from 4.1.3 to 4.2.x. Reverting to 4.1.3 fixes the problem for me, but that's not something i want to keep forever.

I could reproduce on a minimal project, I'm still trying to figure how to fix this (maybe) regression. But looks like the dirty check has been enforced in dev. mode from 4.2.x. But the changelog is not clear about that. Any tip welcome.

jingglang commented 7 years ago

I get the same problem after upgrading from 4.1.3 to 4.2.3. Using setTimeout fixes the problem.

from:

PanictUtil.getRequestObservable().subscribe(data => this.requesting = data);

to:

PanictUtil.getRequestObservable().subscribe(data => setTimeout(() => this.requesting = data, 0));

tytskyi commented 7 years ago

@jingglang could you please try to reproduce the problem on http://plnkr.co/edit/tpl:AvJOMERrnz94ekVua0u5 with as minimal code as possible?

KevinRattan commented 6 years ago

I'm getting the same issue - upgraded from 2.4 to 4.2, and some of my hide/show logic is now broken. It's accordion-style logic, where visibility depends on comparing the current panel with an observable that holds which panel should be visible (amongst other things). The component uses @select to get the observable, and it's bound with | async in the template - worked like a charm in 2.4, now get the ExpressionChangedAfterItHasBeenCheckedError and the panels don't hide and show on first click, only on second.

ver-1000000 commented 6 years ago

I get same issue when updated 4.1.3 to 4.2.2.

But, I found workaround. Injection of ChangeDetectionRef, and call detectionChanges() function at error point.

constructor(private cdr: ChangeDetectionRef) {
}

onChange(): void {
  this.val += 1; // <- get error after
  this.cdr.detectionChanges();
}
MarkPerryBV commented 6 years ago

I think this is an issue with the follow change made in 4.2.

https://github.com/angular/angular/pull/16592

to resolve this issue: https://github.com/angular/angular/issues/14321

Do you have any content inside a ng-content tag?

I have the same issue with a form inside a ng-content tag which now errors with the ContentChangedAfter error.

mmenik commented 6 years ago

I get same issue when updated 4.1.3 to 4.2.0 or higher

I attach a little project for reproduce the bug

app-error.zip

KevinRattan commented 6 years ago

Following up, I downgraded to 4.1.3 (as advised above) and the error goes away, so whatever the conflict is, it's specific to 4.2. I haven't got time to put together a plunkr this week (or next), but it seems to be related to a single action updating two separate observables on the store, each of which has an impact on the UI. One of the UI updates happens, the other causes the exception.

altreze commented 6 years ago

Hmmm, Definitely rollback to version 4.1.3 solves the issue and also apply timeout as shown by @jingglang.

umens commented 6 years ago

hi @tytskyi. I made a plunkr http://plnkr.co/edit/XAxNoV5UcEJOvsAbeLHT for this issue. hope it will help.

the workaround given by @jingglang works (for me) on 4.2.0 or higher OR also changing the emitting event of the child into its constructor don't raise the error anymore

alexzuza commented 6 years ago

@umens You update parent component after it was checked therefore you don't keep unidirection data flow

umens commented 6 years ago

@alexzuza how can I achieve that ? I've always done it this way without the error. Why putting the SetTimout doesn't raise the error anymore. (I updated the plunkr) I doubt it's interfering with the lifecycle or is it ?

tytskyi commented 6 years ago

hi @umens thank you for your time. The problem is what @alexzuza says: you update parent component field after it was checked. Your code is roughly equivalent to this: http://plnkr.co/edit/ksOdACtXScZKw3VRAYTm?p=preview (notice i removed service, to reduce code). Why it worked? Probably by accident old version of angular or Rxjs had bug here. Could you please tell which versions was used? Or even put into working plunker?

Why putting the SetTimout doesn't raise the error anymore.

Because you change field asynchronously, which respects one-way data flow.

Lets consider it from the opposite: Why error is raised? First check of Change Detection goes from up to down. It checks app.toolsConfig value bound to template. Then it renders <child-cmp> which synchronously updates app.toolsConfig. Rendering is done. Now it runs second (dev mode only) check of Change Detection to ensure that application state is stable. But app.toolsConfig before rendering child does not equal app.toolsConfig after. All this happens during single "turn", "cycle" of Change Detection.

umens commented 6 years ago

ok. thank you for the detailed answer. I couldn't find informations about when the child component lifecycle happens. for the previous "working" version I used: @angular/* : 4.1.1 (except compiler-cli -> 4.1.0) rxjs: 5.3.1

tytskyi commented 6 years ago

@umens here is reproduced error with the old versions you mentioned: http://plnkr.co/edit/kfoKmigXzFXwOGb2wyT1?p=preview. It throws, so probably some 3rd party dependency affected behavior or not complete reproduction instruction?

umens commented 6 years ago

can't tell. here is my yarn.lock if you are interested in going further : https://pastebin.com/msARLta1 but i don't know what can be different

saverett commented 6 years ago

I'm seeing similar "ExpressionChanged..." errors after switching from 4.1.2 to 4.2.3.

In my case, I have two components that interact via a service. The service is provided via my main module, and ComponentA is created before ComponentB.

In 4.1.2, the following syntax worked just fine without errors:

ComponentA Template:

<ul *ngIf="myService.showList">
...

ComponentB Class:

ngOnInit() {
  this.myService.showList = false; //starts as true in the service
  ...
}

In 4.2.3, I have to modify ComponentA's template as follows to avoid the "ExpressionChanged..." error:

ComponentA Template:

<ul [hidden]="!myService.showList">
...

I'm still trying to figure out why this has only just become an issue, and why the switch from *ngIf to [hidden] works.

victornoel commented 6 years ago

I have the same problem, and it happens mostly when I'm using an async pipe like this: <div>{{ (obs$ |async).someProperty }}</div> If I use it like this:

<div *ngIf="obs$ | async as o">
  <div>{{ o.someProperty }}</div>
</div>

Then the error disappear, so I'm not so sure it is only because a lot of people made some mistake that wasn't detected before: I think a bug was introduced in 4.2…

cobaltutby commented 6 years ago

The same problem for me. The issue looks like come from @angular/router

KevinRattan commented 6 years ago

I've also come to the conclusion it's related to the router. Tried to post as much yesterday, but it looks like the post failed. I investigated further and simplified the code to see if I could track down the error. The following code works perfectly when the ultimate source of the action is a click on a panel header, but fails with the ExpressionChanged error when triggered via routing.

<span class="glyphicon pull-right" [ngClass]="{'glyphicon-chevron-up' : (ui$ | async)?.visiblePanels[VisiblePanel.IngredientTypes], 'glyphicon-chevron-down' : !((ui$ | async)?.visiblePanels[VisiblePanel.IngredientTypes])}"></span>

acidghost commented 6 years ago

This helped me solving the problem! You have to explicitly trigger the change in the parent.

mpalourdio commented 6 years ago

Like @acidghost, I could solve this problem by running ChangeDetectorRef#detectChanges in AfterViewInit from my parent component.

ghetolay commented 6 years ago

If you have plunkr repro I'm willing to check on it because we had some similar complains on gitter and it always ended up being an user problem.

Can't be sure if a bug was introduced in 4.2 or if the bug was rather the lack of error on < 4.2 and it got resolved on 4.2.

aks4it commented 6 years ago

Error: ExpressionChangedAfterItHasBeenCheckedError solution: componentRef.changeDetectorRef.detectChanges();

Below is a piece from my code (dynamically generated components): componentRef = viewContainerRef.createComponent(componentFactory); //component got created (componentRef.instance).data = input_data; //manually changing component data over here componentRef.changeDetectorRef.detectChanges(); // it will result in change detection

For simple components just google how to access "componentRef" and then execute componentRef.changeDetectorRef.detectChanges(); after setting/changing component data/model.

Solution 2: I was again getting this error while opening a dialog "this.dialog.open(DialogComponent)". so putting this open dialog code inside a function and then applying setTimeout() on that function solved the issue. ex: openDialog() { let dialogRef = this.dialog.open(DialogComponent); } ngOnInit(): void { setTimeout(() => this.openDialog(), 0); } though it seems like hack.. works!!!

Martin-Luft commented 6 years ago

Since 4.2 I have the same problem and no solution or workaround worked for me :( I create a component dynamically with a component factory in a ngAfterViewInit method. This component uses a directive which has an @Input() field. I bind this field to a static string. It seems that the @Input() field of the directive is undefined until the view of the component was build and checked and only then initialized with the static string.

https://plnkr.co/edit/E7wBQXm8CVnYypuUrPZd?p=info

staticComponent.ts

export class StaticComponent implements AfterViewInit {
  @ViewChild('dynamicContent', {read: ViewContainerRef})
  dynamicContent: ViewContainerRef;

  constructor(private componentFactoryResolver: ComponentFactoryResolver) {}

  ngAfterViewInit(): void {
    const componentFactory: ComponentFactory<DynamicComponent> = this.componentFactoryResolver.resolveComponentFactory(DynamicComponent);
    this.dynamicContent.createComponent(componentFactory);
  }
}

test.directive.ts

@Directive({
  selector: '[testDirective]'
})
export class TestDirective {
  @Input()
  testDirective: string;
}

dynamic.component.ts

@Component({
  selector: 'dynamic-component',
  template: `<div [testDirective]="'test'">XXX</div>`
})
export class DynamicComponent {
}

ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'undefined'. Current value: 'test'. It seems like the view has been created after its parent and its children have been dirty checked. Has it been created in a change detection hook ?

stickian commented 6 years ago

I am a bit confused reading this thread. Is this a known bug, which will be resolved in an upcoming release? Or, is this expected behavior? If so, what is the solution? Thank you.

RebelSyntax commented 6 years ago

No idea if it's related or not, but I hate this message with a passion! this ONLY shows up for me when unit testing with the out of the box 'ng test' command. I suppose I could just be confused about how to successfully mock a simple service. Also surprised no one else has seen this in testing yet, can't find anything test related. I'll be watching this topic and any search for ExpressionChangedAfterItHasBeenCheckedError + unit testing.

KevinRattan commented 6 years ago

I've been trying to put together a plnkr, and finding it very difficult to reproduce the error - however, extensive debugging has, I think, got me a bit closer to seeing what's going wrong (though not to deciding whether it's a bug, or a mistake on my part).

I built a small sample app using cut down versions of the same reducers combined in the same order as in the real app, with the routerReducer last. To my frustration, the error did not occur. So I set breakpoints and watched the flow of actions through the reducers. What I found was the two apps were dispatching actions in reverse order.

My UI reducer manages visibility, and is the source of the state change that's triggering the error. Here is a cut-down version of the reducer, showing the relevant actions:


export const userInterfaceReducer = (state = INITIAL_UI(), action: any) => {
    switch (action.type) {
        case IngredientActions.LIST_INGREDIENTS:
            if (action.hideTypes) {
                return Object.assign(state, { visiblePanel: VisiblePanel.Ingredients });
            }
            return state;
        default:
            return state;
    }
}

When I run this in my small test app, it works - first, the LIST_INGREDIENTS action fires and the new state is returned; then the @angular-redux/router::UPDATE_LOCATION action fires, and the default case is hit, returning the same state.

When I run it in the actual app, the order or actions is reversed. @angular-redux/router::UPDATE_LOCATION action fires first, returning the old state from default. Then LIST_INGREDIENTS fires, returning the new (changed) state - and the error is raised.

I am completely open to the idea that I have done something dumb and this isn't an actual bug - but if so, does anyone else see what I've done?

(As a footnote, I checked the 4.1.3 version ... and it also fires the redux reducers in the 'correct' order - that is, the location change fires after the list ingredients action, which presumably is why it works, but the 4.2 version does not).

alexbyk commented 6 years ago

Suffering the same annoying bug when one component changes the property of service. Resolved by moving *ngIf to the parent component. So it's definitely a bug

ghost commented 6 years ago

was able to work around this be using ngDoCheck() method and calling the ChangeDetectorRef.detectChanges method.

public ngDoCheck(): void {
    this.cdr.detectChanges();
}
Toxicable commented 6 years ago

@pappacurds Don't do that. You've caused a infinite Change Detection cycles with that.

elvisbegovic commented 6 years ago

might be same error here https://plnkr.co/edit/IeHrTX0qil17JK3P4GBb?p=preview

error: previous undefined after undefiend

rockcoolt commented 6 years ago

after update same error

Geoide commented 6 years ago

Same for me

matkokvesic commented 6 years ago

Everything fine in 4.0.0, after upgrading to 4.2.6 I get the same error.

maxkoretskyi commented 6 years ago

The article Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedError error explains in depth why this error occurs and possible fixes

cobaltutby commented 6 years ago

fixes are obvious, but changing the framework in such way that update cause user applications to be rewrite to "support" new minor platform update is wired. JS still maintain old mistakes to hold compatibility...

pantonis commented 6 years ago

Same error here

JSMike commented 6 years ago

I'm also seeing this issue in ~4.2.4. Downgrading to ~4.1.3 resolved the issue.

Issue is with changing values of ContentChildren, even when using ChangeDetectorRef detectChanges() at the end of AfterViewInit of the parent (where it was making the changes). Oddly this issue only occurs with components and not directives. I converted a directive that I had working in 4.2.4 to be a component with a template of <ng-content></ng-content> and then it began to throw the ExpressionChangedAfterItHasBeenCheckedError error.

pantonis commented 6 years ago

I got this while working with material 2 input control. Tried to set the value of it

ngAfterViewInit(): void {
this.numberFormControl.setValue(200);
}
saverett commented 6 years ago

I've put together a simple Plunker showing a case where the ExpressionChangedAfterItHasBeenCheckedError errors only started showing up beginning with Angular 4.2.x.

JSMike commented 6 years ago

I've created a simple plunker showing the error when changing a value of a ContentChild even when using ChangeDetectorRef detectChanges() in the parent component's ngAfterViewInit.

Edit: Also created simple plunker based off of my first example but with parent Directive instead of Component and no error.

JSMike commented 6 years ago

@saverett I'm seeing a different error message with your plunk

matkokvesic commented 6 years ago

@JSMike I had exactly the same case as in your plunker (worked around it by wrapping code referencing ContentChild in setTimeout, plunker)

saverett commented 6 years ago

@JSMike Thanks for the heads up. Looks like an issue with angular/zone.js#832. I've set the zone.js version to 0.8.12 for now until they fix the latest zone.js. The Plunker should be working again.

JSMike commented 6 years ago

@matkokvesic that's not really a solution, that just causes the change to occur outside of the lifecycle hook. The code example provided doesn't cause an error when using Angular v4.1.3

matkokvesic commented 6 years ago

@JSMike I agree, It's a temporary workaround. Using elements referenced by ContentChildren in ngAfterViewInit was working fine prior Angular 4.2.6.

Martin-Luft commented 6 years ago
touqeershafi commented 6 years ago

I've fixed it by using changeDetection: ChangeDetectionStrategy.OnPush and this.changeDetector.markForCheck(); in my code