5

I have an app that makes one http request to get a list of items and then makes an http request for each item in the list to get more detailed information about each item. Effectively:

class ItemsService {
  fetchItems() {
    return this.http.get(url)
    .map(res => res.json())
    .map(items => items.map(this.fetchItem(item)));
  }

  fetchItem(item: Item) {
    this.http.get(`${url}/${item.id}`)
    .map(res => res.json());
  }
}

Then I'll do something like itemsService.fetchItems().subscribe(items => console.log(items)) but what ends up happening is I get an array of observables (each response from fetchItem). I need to subscribe to each of the internal observables as well so that the fetchItem request actually gets triggered.

I've also tried using flatMap instead of map but it seems to have the same result in this case. Is there any way for the nested observable to be subscribed to?

3 Answers 3

4

I'd do it like the following:

function mockRequest() {
    return Observable.of('[{"id": 1}, {"id": 2}, {"id": 3}]');
}
function otherMockRequest(id) {
    return Observable.of(`{"id":${id}, "desc": "description ${id}"}`);
}

class ItemsService {
    fetchItems() {
        return mockRequest()
            .map(res => JSON.parse(res))
            .concatAll()
            .mergeMap(item => this.fetchItem(item));
    }

    fetchItem(item: Item) {
        return otherMockRequest(item.id)
            .map(res => JSON.parse(res));
    }
}

let service = new ItemsService();
service.fetchItems().subscribe(val => console.log(val));

See live demo: http://plnkr.co/edit/LPXfqxVsI6Ja2J7RpDYl?p=preview

I'm using a trick with .concatAll() to convert an array of Objects such as [{"id": 1}, {"id": 2}, {"id": 3}] into separate values emitted one by one {"id": 1}, {"id": 2} and {"id": 3} (as of now it's an undocumented feature). Then I use mergeMap() to fetch their content in a separate request and merge it's result into the operator chain.

This plnkr example prints to console:

{ id: 1, desc: 'description 1' }
{ id: 2, desc: 'description 2' }
{ id: 3, desc: 'description 3' }
Sign up to request clarification or add additional context in comments.

4 Comments

So in this case concatAll is sort of like doing mergeMap(item => item)? (or flatMap?)
@ExplosionPills It's the same as mergeMap(items => Observable.from(items))
Why would you need to do Observable.from(items)? If items is an array, flatMap(items => items) will emit each event when it is subscribed to.
@ExplosionPills Hmm, I guess you're right, it produces the same result as concatAll() although undocumented as well (github.com/ReactiveX/rxjs/blob/master/src/operator/… and github.com/ReactiveX/rxjs/blob/master/src/util/…)
2

The problem you likely encountered is that you did not flatten enough.

flatMap or mergeMap will flatten Observables, Promises, Arrays, even generators (don't quote me on that last one), just about anything you want to throw at it.

So when you do .flatMap(items => items.map(item => this.fetchItem(item)), you are really just doing Observable<Array<Item>> => Observable<Observable<Item>>

When you just do map you are doing Observable<Array<Item>> => Observable<Array<Observable<Item>>>.

What you need to do is first flatten out the Array and then flatten out each request:

class ItemsService {
  fetchItems() {
    return this.http.get(url)
    .map(res => res.json())
    // Implicitly map Array into Observable and flatten it
    .flatMap(items => items)
    // Flatten the response from each item
    .flatMap((item: Item) => this.fetchItem(item));
  }
}

Now the above works if you don't mind getting each item response individually. If you need to get all of the items then you should use forkJoin on all the inner values, but you would still need flatMap in order to flatten the resulting inner value:

fetchItems(): Observable<Response[]> {
  return this.http.get(url)
    .map(res => res.json())
    .flatMap(items => {
       const requests = items.map(item => this.fetchItem(item));
       return Rx.Observable.forkJoin(requests);
    });
}

6 Comments

Can you explain more why I would need to do forkJoin? When subscribing to what is returned by fetchItems how would it work different?
@ExplosionPills it creates a stream which emits a single item, an array of all the last values returned from each of the passed in Observables. Your OP made it sound like you wanted all the responses together as a single value in the output instead of getting each response individually.
I do, but in this case I can use .toArray() right?
You could yes, the advantage of forkJoin would be that you only need two operators and the order is automatically maintained which is not the same for .concatMap.mergeMap.toArray
Is fewer operators a particular advantage? In this case it doesn't actually reduce the amount of code if you consider forkJoin to also be an operator, which I would.
|
-1

You can break up the items array before the line that calls this.fetchItem. You can use mergeMap on an Observable whose value is an array and each item will be emitted individually.

fetchItems() {
    return this.http.get(url)
       .map(res => res.json())
       .mergeMap(arrItem => this.fetchItem(arrItem));
}

Edit: I guess I should have provided more explanation. mergeMap is synonymous with flatMap in rxjs. Typically, you use flatMap when your projection function returns an Observable, but it will also flatten arrays as well, so, calling mergeMap will then emit each item individually, I thought that was what OP wanted to achieve. I also realized, you can combine the mergeMap call and last map call, because the projection for mergeMap will be called for each item in the array, I made the changes in the code above.

5 Comments

I believe this should be done with mergeMap, but it looks like you've put it in the wrong place. arrItem is not an observable, why should it be flattened?
I don't fully understand what mergeMap is doing ... how is this different than using .flatMap?
@ExplosionPills It is a synonym. See learnrxjs.io/operators/transformation/mergemap.html
Does this really work? The response from this call is expected to return an array of objects which is emitted as single value. Then calling mergeMap() has no effect because it's called on a single Observable that emits the entire array.
I don't think this fixes the problem; The observable still gets wrapped. You can subscribe to mergeMap, but then observables are emitted and if they are not subscribed to, fetchItem is not initiated.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.