API returns an array that I need to resolve; one request per item

Viewed 215

I have recurring data structure (observable) returned from backend it looks like:

[{
     id:1,
     userId: 111,
     name: '',
     children :[
      { 
        id:3,
        userId: 333,
        name: '',
        children: [...]
      }
     ]
    },
    {
     id:2,
     userId:111,
     name:'',
     children: [...]
   }]

I have another end point that returns user name by user id. I need to call this service wit each of IDs and map returned name to the structure. Is there any pretty solution to achieve this using RxJs operators ?

3 Answers

You can try an approach like the following. See comments inline for details.

// fetchStruct is a function that returns an Observable which notifies the initial structure
const struct$ = fetchStruct();
// here we start the RxJs pipe transformation
struct$.pipe(
  // when struct$ emits the initial structure we pass the control to another observable chain
  // this is done via the concatMap operator
  concatMap(beStruct => {  // beStruct is the structure returned by the back end
    // from beStruct we construct an array of Observables
    // fetchName is a function that returns an Observable that emits when the name is returned
    arrObs = beStruct.map(el => fetchName(el.id))
    // with forkJoin we execute all the Observables in the array in parallel
    forkJoin(arrObs).pipe(
      // forkJoin emits when all Observables have notified and it will emit
      // an array of values with the same order as arrObs
      // we can therefore loop through this array to enrich beStruct with the names
      map(names => {
        names.forEach((n, i) => beStruct[i].name = n);
        return beStruct;
      })
    )
  })
)

This is quite a typical case with RxJs. You may find some other frequent pattern in this blog.

First I'm going to assume that .children can be arrays of ids i.e. pointers to other users. (See example below.)

What I'd would do is transform the initial users array into an array of observables. Each doing a request to map an id to a name. Use merge(…) to fire off the requests.

Of course you don't want a million of HTTP requests going on at the same time so you can set the concurrency parameter of merge to a number that is appropriate to you. (In my example I set it to 2.)

Since you had an array in, you want an array out. You can simply accumulate into an array with the toArray operator.

In the example below:

  1. How I would process the initial users array (i.e. .children should be pointers)
  2. Map users into an array of observables to resolve the names
  3. Merge that array of observables with a concurrency set to 2
  4. Accumulate the result into an array

const get_name = user => fake_backend[user.id].pipe(map(name => ({...user, name})));
//                       ^^^^^^^^^^^^^^^^^^^^^                  ^^^^^^^^^^^^^^^^^
//                       A                                      B
//
// A: Simulate HTTP request latency to lookup an id and return a name
// B: Merge name into initial user object

merge(...users.map(get_name), 2).pipe(toArray()).subscribe(final_users => {
//                            ^       ^^^^^^^^^
//                            A       B
//
// A: Concurrency. Maximum two HTTP requests at any one time.
// B: Accumulate each output into an array. Complete when merge is done.
  console.log(final_users);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/7.2.0/rxjs.umd.min.js" integrity="sha512-MlqMFvHwgWJ1vfts5fdC2WzxDaIXWfYuAd9Tb2lobtF61Gk+HIRDrbtxgasBSM9lZgOK9ilwK9LqFIYEV+k0IA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
const {merge, of} = rxjs;
const {tap, delay, map, toArray} = rxjs.operators;

const users =
  [ { id:       1
    , userId:   111
    , name:     ''
    , children: [3]}

  , { id:       2   
    , userId:   111 
    , name:     ''  
    , children: [4]} 

  , { id:       3
    , userId:   333
    , name:     ''
    , children: []}

  , { id:       4
    , userId:   444
    , name:     ''
    , children: []}];

const fake_backend =
  { 1: of('Foo').pipe(delay(3000), tap(() => console.log('user 1 ✓')))
  , 2: of('Bar').pipe(delay(2000), tap(() => console.log('user 2 ✓')))
  , 3: of('Baz').pipe(delay(1000), tap(() => console.log('user 3 ✓')))
  , 4: of('Bat').pipe(delay(1000), tap(() => console.log('user 4 ✓'))) };
</script>

For your question here is a simple answer.

Steps:

  1. From the list of Users end point you only need id's so you have to simplify the result from the users end point, instead of all the users data you only extract the id's from that end point and keep inside an array.

2.Convert the resultant array into another observable.

3.Send that into mergeMap and there you call a function with arguments to call your second URL service.

Here is a code snippet. -//My first users service

       let obsPattern1$ = this.http.get('https://reqres.in/api/users/');
      obsPattern1$.pipe(
        map(data=>data["data"].map(data=>{
        return data.id
        })),mergeMap(data=> data),mergeMap(data=>this.getUserFromHttp(data))
      ).subscribe(x=>console.log("final result",x))
  }

//this is my second service function to fetch the individual item

getUserFromHttp(id) {
         return this.http.get('https://reqres.in/api/users/' + id);
       }
Related