0

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)?

Vilmantas Baranauskas
  • 6,596
  • 3
  • 38
  • 50
  • I don't fully understand the problem. So when you navigate around the same component is loaded and different `item`s passed. I'd assume you want to cancel a request when the data hasn't yet arrived when you navigate to a different item. What if you navigate back? I guess you want to get the item from a cache (from the previous request). I would create a service that ensures previously loaded items are immediately available. For example like shown in http://stackoverflow.com/questions/36271899/what-is-the-correct-way-to-share-the-result-of-an-angular-2-http-network-call-in – Günter Zöchbauer Nov 24 '16 at 15:07
  • Cache does not solve the original problem. Only single SomeObjectComponent instance is initiated here and then the value of "currentlySelected" property of the container changes and is propagated via the [item] binding. One solution would to store subscription and cancel it, another to check for "this.someObject === v" within subscribe. I wanted to know what are possible options and what would be the best one, especially when we have multiple extra data items. Is there some magic RxJS solution here? – Vilmantas Baranauskas Nov 24 '16 at 15:13
  • Ok, that makes it more clear. Cache doesn't solve the original problem, but I was curious if this is a requirement as well. – Günter Zöchbauer Nov 24 '16 at 15:16

1 Answers1

1

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.

olsn
  • 16,644
  • 6
  • 59
  • 65