4

I have been using Angular/RxJS for a few weeks and have various models that are built from multiple REST requests which I have thus far achieved using switchMap(). Here is a simple contrived example (stackblitz: https://stackblitz.com/edit/angular-o1djbb):

import { Component, OnInit, } from '@angular/core';
import { Observable, of } from 'rxjs';
import { delay, switchMap } from 'rxjs/operators';

interface Order {
  id: string;
  itemName: string;
  details?: string;
}

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  order: Order;

  ngOnInit() {
    this.getOrderFromApi(123)
      .subscribe(item => this.order = item);
  }

  getOrderFromApi(id): Observable<Order>  {
    const item$ = this.getItemName(id);
    const fullItem$ = item$.pipe(
      switchMap(n => {
        console.log(`Got name: '${n}''`);
        return this.getDetails(n);
      },
        (nameReq, detailsReq) => ({ id: '123', itemName: nameReq, details: detailsReq })
      ));
    return fullItem$;
  }

  getItemName(id): Observable<string> {
    return this.fakeXhr('foo');
  }

  getDetails(itemName): Observable<string> {
    console.log(`Got details '${itemName}''`)
    return this.fakeXhr('Some details about foo');
  }

  fakeXhr(payload: any) {
    return of(payload)
      .pipe(delay(2000));
  }
}

And a simple template:

<p>
  item: {{order && order.itemName}}
</p>
<p>
  details: {{order && order.details}}
</p>

This works but the order info is not rendered until both requests complete. What I would like to happen is for the itemName to render as soon as it's available and then details to render when they become available. Hence taking advantage of the multiple values that Observables can emit. Eg:

// first value emitted:
{ itemName: 'foo', details: null }
// second value emitted:
{ itemName: 'foo', details: 'Some details about foo' }

I realise I could probably achieve this with a BehaviourSubject or using Redux (as I have in the past with React), but feel there is a simple solution I can't quite grasp due to the newness of all this for me.

Jai
  • 63
  • 6

4 Answers4

2

Use expand to emit the data from the first request straight away and subsequently do the second request.

expand will call the inner request recursively, getting the order from the last request as input, so we only execute the second request when the order has no details and end the recursion otherwise with an EMPTY Observable.

import { Observable, EMPTY } from 'rxjs';
import { map, expand } from 'rxjs/operators';

getOrderFromApi(id): Observable<Order> {
  return this.getItemName(id).pipe(
    map(itemName => ({ id, itemName, details: null } as Order)),
    expand(order => order.details
      ? EMPTY
      : this.getDetails(order.itemName).pipe(
        map(details => ({ id, itemName: order.itemName, details } as Order))
      )
    )
  );
}

https://stackblitz.com/edit/angular-umqyem

The returned Observable will emit:

  1. { "id": 123, "itemName": "foo", "details": null }
  2. { "id": 123, "itemName": "foo", "details": "Some details about foo" }
frido
  • 13,065
  • 5
  • 42
  • 56
  • I'm marking this as the answer as I think it's the most elegant solution (currently at least). We could take this further and use the _index_ from expand something like so: https://stackblitz.com/edit/angular-btso9u?file=src/app/app.component.ts – Jai Feb 04 '19 at 21:18
  • Thanks, `expand` is also well suited for executing an unknown amount of requests like when you're dealing with pagination. https://stackoverflow.com/a/54293624/9423231 – frido Feb 05 '19 at 11:42
0

Maybe your best choice is to substitute mergeMap() for switchMap()?

https://blog.angular-university.io/rxjs-higher-order-mapping/

As we can see, the values of the merged source Observables show up immediately in the output. The result Observable will not be completed until all the merged Observables are completed.

I would definitely check out these articles:

paulsm4
  • 114,292
  • 17
  • 138
  • 190
  • A direct substitute won't certainly won't work: https://stackblitz.com/edit/angular-f8canj?file=src/app/app.component.ts Fork that stackblitz and show us how! – Jai Feb 03 '19 at 00:11
0

Instead of persisting your full item at the end of the observable stream (i.e. in your .subscribe block) you could store the received values within the respective parts of your code, namely after they are available:

 switchMap(n => {
        console.log(`Got name: '${n}''`);
        this.order.name = n;
        return this.getDetails(n);
      },

... and then in your sibscription block you would only have to store the advanced information:

this.getOrderFromApi(123)
      .subscribe(item => this.order.details = item.details);

Obviously, this requires you to initialize your ´order´ attribute accordingly, e.g. with order: Order = {}; or order: Order = new Order();.

Basically, the idea is to restructure your code s.t. your observable stream emits partial values (your order's attributes) instead of a full-blown object.

mitschmidt
  • 496
  • 3
  • 11
  • This might work in this contrived example, but in reality the getOrderFromApi() would be in a service and then the only way to achieve the above would be to pass a reference to an object into it _getOrderFromApi(this.order)_ which is not really what I was after. I want multiple values emitted from the returned Observable. – Jai Feb 04 '19 at 10:43
0

One solution I did come up with was to just create an Observable and call next(). I'd be interested to hear any opinion on this.

getOrder(id) {
    const ob$ = new Observable(subscriber => {
      const item$ = this.getItem(id)
        .pipe(
          tap(o => subscriber.next(({ id: id, itemName: o.itemName, details: null }))),
          switchMap(o => this.getDetails(o.itemName),
            (o, d) => ({ id: id, itemName: o.itemName, details: d.details })
          ),
          tap(a => subscriber.next(a))
        );
      item$.subscribe(a => {
        subscriber.complete();
      });
    });

    return ob$;
  }

Stackblitz: https://stackblitz.com/edit/angular-mybvli

Jai
  • 63
  • 6