Search code examples
angulartypescriptdata-bindingerror-handlingangular2-changedetection

Angular observable on error does not refresh interface with ChangeDetectionStrategy.OnPush


I have a component that uses a service to submit some data.

This component uses ChangeDetectionStrategy.OnPush, but something strange is happening:

  • When request succeeds: next callback is invoked, formStatus changes to 'success' and interface refreshes with this change.

  • When request fails, error callback is invoked, formStatus changes to 'error' but interface does not refresh with this change.

As I understand, interface should not refresh because I use ChangeDetectionStrategy.OnPush and I am changing a basic type string variable.

So, ¿why change detection is triggered in next?

Component

When some error happens in the service, I want to receive it inside the error callback.

submit() {
    this.formStatus = 'sending';
    this._service.create(data).subscribe({
      next: () => {
        this.formStatus = 'success';
        // interface refreshes
      },
      error: () => {
        this.formStatus = 'error';
        // interface does not refresh
        console.log(this.formStatus); // prints 'error'
      },
    });
  }
<div *ngIf="formStatus === 'success'">
  ...
</div>

Service

Makes a call to the API and catches the error using errorUtil.handle, which throws an rxjs Error (because I want to return Observable.error).

  create(requestData: Data): Observable<Data> {
    return this._http
      .post<HttpResponse<CreateResponse>>(
        MY_API.createUrl,
        JSON.stringify(requestData)
      )
      .pipe(
        catchError(this._errorUtil.handle()),
        map(data => this.onSuccessCreate(requestData, data))
      );
  }

Error Handler

handle<T>(): (error: HttpErrorResponse, caught?: Observable<T>) => Observable<T> {
    return (error: HttpErrorResponse, caught?: Observable<T>) => {
      return throwError();
    };
  }

I have tried changing return throwError(); to throw throwError(); but the behaviour is the same.


Solution

  • This is a weird behaviour, I still do not understand why when next is invoked, interface is updated. It should not be updated either.

    The right way to do this is using a stream to listen to changes in form status.

    Component

    
    type FormStatus = 'clean' | 'sending' | 'success' | 'error';
    // ...
    private _formStatus$: BehaviorSubject<FormStatus> = new BehaviorSubject<FormStatus>('clean');
    
    
    set formStatus(status: FormStatus) {
        this._formStatus$.next(status);
    }
    
    get formStatus$(): Observable<FormStatus> {
        return this._formStatus$.pipe(
            shareReplay({ refCount: true, bufferSize: 1 })
        );
    }
    
    <div *ngIf="(formStatus$ | async) !== 'error'">
      ...
    </div>