Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Error: ExpressionChangedAfterItHasBeenCheckedError when an Angular Component is tested with synchronous Observables

I have a simple component which uses a service returning an Observable. Such Observable is used to control the creation of an html element via *ngIf.

Here the code

@Component({
  selector: 'hello',
  template: `<h1 *ngIf="stuff$ | async as obj">Hello {{obj.name}}!</h1>`,
  styles: [`h1 { font-family: Lato; }`],
})
export class HelloComponent implements AfterViewInit {
  constructor(
    private myService: MyService,
  ) {}

  stuff$;

  ngAfterViewInit() {
    this.stuff$ = this.myService.getStuff();
  }
}


@Injectable({
  providedIn: 'root'
})
export class MyService {
  getStuff() {
    const stuff = {name: 'I am an object'};
    return of(stuff).pipe(delay(100));
  }
}

As you can see, getStuff() returns and Observable with a delay and everything works as expected.

Now I remove the delay and I get the following error on the console

Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'ngIf: null'. Current value: 'ngIf: [object Object]'.

This is a Stackbliz that reproduces the case.

Is this inevitable? Is there a way to avoid this error? This small example replicates a real world example, where MyService queries a back end, so the delay.

This is for me relevant because I would like to be able to run tests in a synchronous way simulating the real backend and, when I try such an approach, I get the same result.

like image 455
Picci Avatar asked Dec 23 '22 01:12

Picci


1 Answers

The error basically says that one change triggered another change. It happens only without delay(100) because RxJS is strictly sequential and subscriptions happen synchronously. If you use delay(100) (even if you use just delay(0) it makes the emission asynchronous so it happens in another frame and is caught by another change detection cycle.

A very simple way to avoid this is just wrapping assigning to a variable with setTimeout() or with NgZone.run().

Or more "Rx way" is using subscribeOn(async) operator:

import { asyncScheduler } from 'rxjs';
import { observeOn } from 'rxjs/operators';

...

return of(stuff).pipe(
  observeOn(asyncScheduler)
);
like image 93
martin Avatar answered May 06 '23 16:05

martin