Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ExpressionChangedAfterItHasBeenCheckedError in two-way Angular binding

Here's an example:

@Component({
  selector: 'my-app',
  template: `
    <div>
      <h1>{{ foo }}</h1>    
      <bpp [(foo)]="foo"></bpp>
    </div>
  `,
})
export class App {
  foo;
}

@Component({
  selector: 'bpp',
  template: `
    <div>
      <h2>{{ foo }}</h2>
    </div>
  `,
})
export class Bpp {
  @Input('foo') foo;

  @Output('fooChange') fooChange = new EventEmitter();

  ngAfterViewInit() {
    const potentiallyButNotNecessarilyAsyncObservable = Observable.of(null);

    potentiallyButNotNecessarilyAsyncObservable.subscribe(() => {
      this.fooChange.emit('foo');
    })
  }
}

where error occasionally appears:

ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'undefined'. Current value: 'foo'

It results from the fact that two-way binding is updated by an observable that can get a value on same tick. I would prefer to not wrap the logic above with setTimeout because it looks like a hack and complicates control flow

What can be done to avoid this error here?

Does ExpressionChangedAfterItHasBeenCheckedError error have ill effects or can it be ignored? If it can, can change detector be silent on it and not pollute the console?

like image 654
Estus Flask Avatar asked Sep 06 '17 01:09

Estus Flask


1 Answers

Let's first unwrap the two-way data binding to simplify explanation:

<div>
  <h1>{{ foo }}</h1>    
  <bpp [foo]="foo" (fooChange)="foo=$event"></bpp>
</div>

It still has the same effect and occasionally produces the error. The error will only be produced if the potentiallyButNotNecessarilyAsyncObservable is synchronous. So we can also replace this:

ngAfterViewInit() {
    const potentiallyButNotNecessarilyAsyncObservable = Observable.of(null);

    potentiallyButNotNecessarilyAsyncObservable.subscribe(() => {
      this.fooChange.emit('foo');
    })

with this:

ngAfterViewInit() {
    this.fooChange.emit('foo');

This case falls into the Synchronous event broadcasting category of errors that is explained in the article Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedError error.

The ngAfterViewInit lifecycle hook is triggered after the parent component changes have been processed. The order of hooks related to child components is explained in the Everything you need to know about change detection in Angular. Now Angular remembers that when it ran change detection for the App component the value of foo was undefined, but during validation phase the value is foo that is updated by the child Bpp component. Hence it produces the error.

What can be done to avoid this error here?

The fixes and the problems are described in the article I linked. The only safe option here if you don't want to redesign you logic is asynchronous update. You could also run change detection for the parent component but they may lead to an infinite loop since change detection on a component triggers change detection for component's children.

Does ExpressionChangedAfterItHasBeenCheckedError error have ill effects or can it be ignored?

The ill affect is that you will have incosistent state in the application App.foo==='foo' and the view {{foo}}===undefined until the next digest cycle iteration. The error cannot be turned off in the development mode but it will not appear in the production mode.

Two Phases of Angular Applications is also pretty good in explaining the mental model for this error.

like image 73
Max Koretskyi Avatar answered Oct 23 '22 03:10

Max Koretskyi