Unlocking the power of RXJS by de-nesting subscriptions

Trying to resolve the nested subscription issue has become a time-consuming puzzle. I've experimented with mergeMap, flatMap, and switchMap without success. Unfortunately, the examples I've come across don't quite fit my needs, leaving me with only one result, undefined, or an error. The snippet of code that requires fixing is as follows:

this.myService.getAll().subscribe(res => {

// res.result is an array of 20 objects
res.result.forEach(m => {
    // for each object, two different endpoints are called to add a key with the response
    this.myService.checkFirst(m.id).subscribe(result => {
        m.first = result;
    });
    this.myService.checkSecond(m.id).subscribe(result => {
        m.second = result;
    });
});
// once all subscriptions are complete, the mapped array should be returned
this.dataLength = res.total;
});

Answer №1

Give it a shot

this.myService.getAll().pipe(
  switchMap(res => {
    const obs$ = res.result.map(m => {
      return this.myService.checkFirst(m.id).pipe(
        map(first => ({...m, first})),
      );
    });
  
    return forkJoin(obs$).pipe(
      map(result => ({...res, result})),
    ),
  }),
  switchMap(res => {
    const obs$ = res.result.map(m => {
      return this.myService.checkSecond(m.id).pipe(
        map(second => ({...m, second})),
      );
    });
  
    return forkJoin(obs$).pipe(
      map(result => ({...res, result})),
    ),
  }),
).subscribe(/* ... */);

Answer №2

If I understand your issue correctly, my approach would be as follows:

Assuming that this.myService.getAll() is an http call, the goal is to perform actions once this call returns and the related Observable completes. The operator to achieve this is concatMap, which allows you to work with subsequent Observables after the source one (this.myService.getAll()) completes.

After retrieving the result of this.myService.getAll(), you need to make 2 calls for each item in the returned array. These calls can run concurrently and update properties of the item as a side effect.

To execute 2 calls concurrently, you can utilize the forkJoin function, which emits an array with the results of both calls after they complete. Therefore, the following code snippet should handle the task for a single item:

forkJoin([this.myService.checkFirst(m.id), this.myService.checkSecond(m.id)]).pipe(
   tap(([first, second]) => {
     m.first = first;
     m.second = second;
   })
)

Since you have 20 items in the array, you will need to repeat the above logic 20 times, possibly concurrently. In such cases, you can use forkJoin again to process requests for each item in the array.

Putting it all together, your solution might look something like this:

this.myService.getAll().pipe(
  concatMap(res => {
    const requestForItems = res.result.map(m => 
      forkJoin([this.myService.checkFirst(m.id), this.myService.checkSecond(m.id)]).pipe(
        tap(([first, second]) => {
          m.first = first;
          m.second = second;
        })
      )
    )
    
    return forkJoin(requestForItems).pipe(
      map(() => res)
    )
  })
)
.subscribe(res => // res.result now contains items enriched with data from the service)

For more insights on handling Observables and http scenarios, you may find this article on Observable and http patterns engaging.

Answer №3

One advantage of using RxJS is the ability to nest streams as deeply as needed. This means that you can create a stream to enrich a single object and nest multiple streams to enrich an entire array.

For example, if we have a stream that enriches a single object and prints it to the console:

const oneObject = getObject();
forkJoin({
  firstResult: this.myService.checkFirst(oneObject.id),
  secondResult: this.myService.checkSecond(oneObject.id)
}).pipe(
  map(({firstResult, secondResult}) => {
    oneObject.first = firstResult;
    oneObject.second = secondResult;
    return oneObject;
  })
).subscribe(
  console.log
);

If the oneObject is itself returned from an observable, we can merge or switch our object into the same stream created above:

this.myService.getOneObject().pipe(
  mergeMap(oneObject => 
    forkJoin({
      firstResult: this.myService.checkFirst(oneObject.id),
      secondResult: this.myService.checkSecond(oneObject.id)
    }).pipe(
      map(({firstResult, secondResult}) => {
        oneObject.first = firstResult;
        oneObject.second = secondResult;
        return oneObject;
      })
    )
  )
).subscribe(
  console.log
);

To extend this functionality to an entire array of objects, we can utilize the forkJoin operator to run an array of observables concurrently:

this.myService.getAll().pipe(
  map(allRes =>
    allRes.result.map(m => 
      forkJoin({
        first: this.myService.checkFirst(m.id),
        second: this.myService.checkSecond(m.id)
      }).pipe(
        map(({first, second}) => {
          m.first = first;
          m.second = second;
          return m;
        })
      )
    )
  ),
  mergeMap(mArr => forkJoin(mArr)),
).subscribe(resultArr => {
  // Log the enriched results for the first object in the array.
  console.log(resultArr[0].first, resultArr[0].second)
});

We can simplify the previous solution by combining the map and mergeMap into a single mergeMap:

this.myService.getAll().pipe(
  mergeMap(allRes =>
    forkJoin(allRes.result.map(m => 
      forkJoin({
        first: this.myService.checkFirst(m.id),
        second: this.myService.checkSecond(m.id)
      }).pipe(
        map(({first, second}) => {
          m.first = first;
          m.second = second;
          return m;
        })
      )
    ))
  )
).subscribe(console.log);

If there are concerns about the completion of checkFirst and checkSecond, consider using zip instead of forkJoin and unsubscribe with take(1) or first():

this.myService.getAll().pipe(
  mergeMap(allRes =>
    forkJoin(allRes.result.map(m => 
      zip(
        this.myService.checkFirst(m.id),
        this.myService.checkSecond(m.id)
      ).pipe(
        first(),
        map(([first, second]) => {
          m.first = first;
          m.second = second;
          return m;
        })
      )
    ))
  )
).subscribe(console.log);

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Logging to the console using an If/Else statement, with the Else block containing code that

I'm encountering a peculiar issue with my If statement. I'm modifying the state (true/false) of an object based on onMouseEnter and onMouseLeave. I can execute the code when it's true, but if I include any code in the else statement, it ma ...

Can the default Bootstrap classes in the ExtLib application layout control be modified?

I am currently using the responsive Bootstrap theme in combination with the application layout control in extlib, but I have been unable to locate any documentation on whether it is possible to modify the predefined Bootstrap classes. In the example below ...

Issue with Jquery's .html() function not functioning properly when trying to select HTML

I am currently working on a piece of code that looks like this: $price = $(element) > $('.main_paket_price').attr('name'); alert($price); Basically, I am trying to select an element inside element which has the class main_paket_pri ...

Looking for a way to extract a dynamic URL from a website's div element?

Is there a way for my app to load dynamically by extracting and reading the changing URL from a webpage? //webpage <div style="display:none" id="urladdress"> //dynamic url **https://freeuk30.listen2myradio.co ...

Failed to load JSON data from the factory

Recently, I started learning AngularJS and have been struggling to fetch JSON data from a factory. The error message I keep getting is not very helpful: TypeError: Cannot read property '1' of null This is the module I am working with: var app ...

Tips for showcasing overflowing text in a menu list by rotating the item text

Imagine you have a TextMenuItem component, using MenuItem from the Material-UI library, that is part of a chain consisting of DropDownSearch > SimpleListMenu > FixedSizeList > TextMenuItem. In simple terms, this creates a searchable dropdown eleme ...

Integrating JSON with the DOM

Currently, I am searching for a library that offers a simple method to bind JSON data to existing DOM elements that have been generated by a Rails view template. The main reason behind this requirement is that my application features in-place editing (uti ...

Could you show me how the easyrtcid is generated in the demonstration of audio-only chat? I would appreciate a step-by

Currently, I am utilizing the easyrtc webpage to test out the audio only chat demo and everything seems to be running smoothly. However, when connecting, the easyrtcid variable is automatically assigned a random string. I was wondering if there is a way t ...

issue occurring after inserting a new parameter

I encountered an issue with my test case after adding a new parameter tiger to the method swimming. Despite passing the new parameter tiger to my test case, it continues to break. Update: I am receiving an "undefined grid" error at this line. Any suggest ...

The VueJS Chosen directive fails to refresh when new options are selected

Struggling to populate my jQuery Chosen dropdown field with AJAX data using VueJS. Unfortunately, when trying to update the values, the Chosen dropdown does not reflect the changes. I've experimented with different approaches, including manually trig ...

What is the best way to link together Angular observables?

In order for my component to make API requests, it needs to check if certain app preferences are set. Currently, I have implemented a method where the component's data is refreshed every 2 minutes using a timer: ngOnInit(): void { this.subscriptio ...

When an SVG image is embedded, its color may not change even after being converted to an inline SVG

I've inserted an SVG using an img tag. When hovering over it, I want the fill color of the SVG to change. I attempted to convert the SVG to inline SVG following this method, but it doesn't seem to be working as expected. No console errors are b ...

Modifying Image on Tab Click using jQuery

In my current WordPress project, I am working on dynamically changing an image based on the tab that is clicked. I would like to use jQuery's fade effect to smoothly replace the image with a new one that is relative to the specific tab being clicked. ...

Tips for sending AngularJS expressions to a controller

I am looking to pass a value from an AngularJS Expression to the controller. Here is the HTML code : <div data-ng-controller="AlbumCtrl"> <div data-ng-repeat="z in songInfo"> <div data-ng-repeat="b in z.album& ...

Instructions for removing the status bar from the configuration file

Struggling to remove the status bar that's covering my header in my Phonegap-wrapped HTML5 mobile app. I've tried adding preferences to the config.xml file, but still no luck. Here's what I added: <preference name="fullscreen" value="tr ...

Creating numerous pre-signed URLs using an Application Programming Interface

An API has been developed to generate pre-signed URLs for files stored in a private S3 bucket. The goal is to store these URLs in an array for access from another application. ["FILE1 pre-signed URL", "FILE2 pre-signed URL", etc..] However, there seems to ...

Table 1 must retain its value permanently following any modifications to the inputs

I have recently started learning React and am facing an issue while trying to dynamically add table rows on button click. The problem I'm encountering is that the values from the input fields are being directly added to the table rows, and any changes ...

Assessing the string to define the structure of the object

Currently, I am attempting to convert a list of strings into a literal object in Javascript. I initially tried using eval, but unfortunately it did not work for me - or perhaps I implemented it incorrectly. Here is my example list: var listOfTempData = [ ...

Implementing dynamic checkbox values depending on a selection from a dropdown menu in Angular

In my checkbox list, I have various Samsung mobile models and two offers available. $scope.offers = [ { id: "as23456", Store: "samsung", Offer_message:"1500rs off", modalname: "Samsung Galaxy You ...

Utilizing PHP to fetch data from a separate webpage

This is a question that has sparked my curiosity. I am not facing any particular issue that requires an immediate solution nor do I possess the knowledge on how to achieve it. I have been contemplating whether it is feasible to utilize PHP for fetching co ...