dagda1
dagda1

Reputation: 28910

use rxjs to observe changes in an array in angular 4

I have the following property in a component:

  get isUploading() {
    return this.files.length > 0 && this.files.some((f) => f.state === FileUpLoadState.uploading);
  }

I then have a progress component that is bound to that property:

<md-progress-bar *ngIf="this.isUploading" mode="indeterminate">loading...
</md-progress-bar>

I have an upload function that loops through an array of file objects and directly mutates the array of objects:

 upload(){
    this.files.forEach(async (uploadedFile) => {
      uploadedFile.state = FileUpLoadState.uploading;

      try {
        uploadResponse = await this.blobStorageService.upload(uploadedFile.file, {
          maxSingleShotSize,
          progress: (progress) => file.progress = progress.
        });

        uploadedFile.state = FileUpLoadState.success;

        this.uploadActions.persistUpload({ ...uploadedFile, ...uploadResponse });
      } catch (err) {
        uploadedFile.error = err.message;
        uploadedFile.state = FileUpLoadState.error;
      }
    });
  }

I am coming from the react world were observing property changes like this is not something you do.

Would using an rxjs observable be a better way or what is the common idiom for doing this in angular?

Upvotes: 2

Views: 1080

Answers (1)

Reactgular
Reactgular

Reputation: 54821

I'll try to explain what's happening now and why it's not recommended and an alternative approach.

get isUploading() {
   return this.files.length > 0 && this.files.some((f) => f.state === FileUpLoadState.uploading);
}

The above is a state property where the value is mutated asynchronously by the upload(). Angular will need to render the component's view to reflect any changes in the HTML. This is all done automatically via change detection, and the reason you're seeing these changes is because Angular uses zones to detect async operations which trigger change detection.

https://www.joshmorony.com/understanding-zones-and-change-detection-in-ionic-2-angular-2/

Your example works, because you're creating promises inside an Angular zone. When you're inside a zone and you create an async listener, then Angular will mocky patch the operation so that Angular change detection happens when the async operation completes.

await this.blobStorageService.upload(...)

So the above line of code is executed inside a zone, and when async completes Angular will walk down all of the component views to see if any have changed. It will then render any components that are dirty.

I am coming from the react world were observing property changes like this is not something you do.

You definitely wouldn't do this in React, and I can't honestly say that I like this aspect of Angular.

Out of the box Angular requires less understanding of how views, zones and change detection works. You can do things like this and quickly create a working application, but it does not scale up to a larger application. As you add more and more views from components then change detection slows down and begins to lag.

We call this technical debt because if your application grows in size, then you'll have to go back and refactor things to use OnPush change detection. After you've been burned by this feature in Angular, then you'll always use OnPush from there afterwards.

https://netbasal.com/a-comprehensive-guide-to-angular-onpush-change-detection-strategy-5bac493074a4

As a React developer. You've spotted this very early and it just feels wrong but you don't understand why, and I think that really demonstrates why React has become so popular. It solves an important problem in Angular by never allowing the problem to exist in the first place.

I just want to know if this is how angular devs do this kind of thing or do they use rxjs

So the recommended approach is to use OnPush change notification. Once the component is configured to use OnPush then it's view will not be updated after the async operation is completed. Angular will not detect changes because none of the input bindings have changed.

Now we can actually answer your question better. How do you update the view when an async operation has changed?

Mark the view dirty

You can inject the ChangeDetectRef class into the component, and then mark the view as dirty.

https://angular.io/api/core/ChangeDetectorRef#markforcheck

This is by far the easiest solution when Reactive programming requires that you refactor the source code, or you can't use reactive programming at all.

    this.uploadActions.persistUpload({ ...uploadedFile, ...uploadResponse });
    this.changeDetectorRef.markforcheck();

You would just add one line to your code and this will tell Angular that the component is dirty.

I wouldn't say that this one approach is a best practice, but it's how you would make it work with OnPush and that's what it's important. This component will now only need updating when it's actually changed, and Angular won't have to detect changes all the time.

Reactive programming with async

Reactive programming allows you to update the view using the async pipe.

https://angular.io/api/common/AsyncPipe

The advantage is that the pipe handles marking the view as dirty. So you can create a component that uses OnPush and responds to async operations without having to manually call markforcheck() all the time.

Your template would become:

<md-progress-bar *ngIf="isUploading | async" mode="indeterminate">loading...

The property would then be an observable that emits true/false values. There are many ways to convert your upload() function to emit the value, but I don't want to attempt to rewrite it for you. I think that's not really what the question is about.

Upvotes: 2

Related Questions