Vilmantas Baranauskas
Vilmantas Baranauskas

Reputation: 6726

Loading of slow sub-resources in Angular2

Angular2 component displays some object. In addition to base object info, extra data needs to be loaded via a service which returns Observable and is quite slow.

Base part of the object info should be displayed immediately and the extra data once available.

Something like this:

SomeObjectComponent {

    someObject: SomeObject;
    extraData: ExtraData;

    @Input()
    set item(v: SomeObject) {
        this.someObject = v;
        backendService.getExtraData(v.id)
            .subscribe(d => this.extraData = d);
    }        

}

// in the template
<some-object [item]="currentlySelected"></some-object>

When loading is slow and user navigates between different values of SomeObject, asynchronous loading may load and assign wrong extraData for the current item.

What would be the best way to solve this? What if multiple extra data items need to be loaded each time (and each appearing as soon as loaded)?

Upvotes: 0

Views: 146

Answers (1)

Olaf Horstmann
Olaf Horstmann

Reputation: 16892

If I understood you correctly, basically what you are looking for is a way to cancel "old" rest-calls, as soon as a new item is selected -> the rxjs-way would be to utilize an .takeUntil(...)

SomeObjectComponent implements OnDestroy {
    // to clean up any open subscriptions it is helpful to have a destroy-event
    private destroyed$: Subject<any> = new Subject<any>();

    // your vars
    someObject: SomeObject;
    extraData: ExtraData;

    @Input()
    set item(v: SomeObject) {
        this.someObject = v;
        this.loadExtraData$.next(v.id);
    }        

    // a trigger to load extra data
    private loadExtraData$: Subject<number> = new Subject<number>();  // type "number" is just an assumption, this can be whatever type "v.id" is

    // the stream to load extra data
    private extraData$: Observable<any> = this.loadExtraData$
        .takeUntil(this.destroyed$) // when the component is destroyed this will ensure that any open subscription will be closed as well to prevent memory leaks
        .switchMap(id => {
            return this.backendService
                .getExtraData(id)
                .takeUntil(this.loadExtraData$);  // will discard the rest-call the next time loadExtraData$ is triggered (if it is still running)
        })
        .do(extraData => this.extraData = extraData) // if you only need the data in the template, then don't use this line and use the "| async"-pipe instead
        .share();

    // this is called by the angular2-lifecycle, when the component is removed
    ngOnDestroy() {
        this.destroyed$.next();
    }
}

If you just need the extra data in the template, then you might want to use the async-pipe:

<span>Some extra data: {{(extraData$ | async)?.someAttribute}}</span>

As a small side-note and slightly off-topic: It is a better practice to not load the extra data within the component, but outside of the component and use an @Input() for the extra-data, for a number of reasons:

  • testability is much more efficient and easier to achieve if components are encapsulated and don't rely on any service or other components
  • especially with rest-calls there is a danger of making redundant requests if a component is added twice e.g.
  • components are much easier to reuse in different contexts if they just have @Input()s and @Output()s

That being said, without knowing your current architecture, this might bear bigger refactoring that might not be worth the time, but it is something to keep in mind for future applications.

Upvotes: 1

Related Questions