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

In ReactJS, when passing an array of objects to a child component, the first instance may sometimes return as undefined, causing the code to break

Currently, I am in the process of creating a personal blog for my brand. The "Posts" section is designed to retrieve data from a basic JSON via an API. At present, I have used mirageJS to mock the API and employed the useEffect and useState hooks to set th ...

Is it possible to use jQuery to refresh only a section of the page and modify the URL at the same time?

There is a page (http://myflashpics.com/picture/p9e0) that displays user information along with a small thumbnail on the side. Currently, when clicking on the image, it redirects to a different page and the sidebar reloads as well. I am curious if it' ...

Having trouble adjusting the color on Material UI select in ReactJS?

Here is the code snippet I am working with: const useStyles = makeStyles({ select: { '&:before': { borderColor: 'white', }, '&:after': { borderColor: 'white&apos ...

Changing the border of an iframe element using jQuery or Javascript from within the iframe itself

Is it possible to set the border of an iframe to zero from within the iframe itself using JavaScript or jQuery? Any guidance on how this can be achieved would be greatly appreciated. Thank you! ...

Customize AngularJS checkbox values when checked

Is there a way to set a checkbox to be checked by default with a custom value instead of a boolean value? Ready to go? <input type="checkbox" ng-model="checked" ng-init="checked=true" ng-true-value="sure" ng-false-value="nope"><br/> <tt> ...

Attempting to navigate through nested data within existing mapped data

The data set 1 consists of an array that contains another array called data set 2. Currently, data set 1 is being mapped to display a single-column table with data1.name inside it. The data1.name serves as a clickable button that reveals the related data i ...

Set the minimum height of a section in jQuery to be equal to the height of

My goal is to dynamically set the minimum height of each section to match the height of the window. Here is my current implementation... HTML <section id="hero"> </section> <section id="services"> </section> <section id="wo ...

Create and adapt dynamic tiles to fit within the available width

I am looking to create a dynamic tile (div) that adjusts based on the number of users available, similar to how it works in Microsoft Teams meetings. For example, if there is only one user, the div should occupy the full screen. When there are two users ...

Storing data on your local machine using Electron

I am in need of help with my template files which have variable strings. I want to create a basic input form using Electron (https://www.electronjs.org/) and save the resulting output file on the user's device. Could someone recommend a module that e ...

Split an array of simple data types in JavaScript into separate sections

Is there a way to divide an unordered array of primitive types into specific segments like this: var array = [102,103,104,201,203,204,303,301,302,405,406,408,101]; => newArray = [[101,102,103,104],[201,203,204],[303,301,302],[405,406,408]] The divisio ...

Angular fails to include the values of request headers in its requests

Using Django REST framework for the backend, I am attempting to authenticate requests in Angular by including a token in the request headers. However, Angular does not seem to be sending any header values. Despite trying various methods to add headers to ...

Why is the click event not working in IE8 for JavaScript / jQuery?

There seems to be an issue with the program not functioning properly in IE8 for some users. The website in question is an English course where certain individuals are experiencing difficulty clicking on the correct answers. To view a demonstration of this ...

Sharing package JSON file dependencies with child engines and addons in Ember.js

I am seeking information on how Ember Js can share the parent app's package.json file dependency (xyz:3.0.0) with child engines and addons without them needing to redeclare the dependencies in their own package.json files. This is to reduce the overal ...

Utilize TinyMCE in your WordPress plugin

How can I integrate TinyMCE into my WordPress plugin? I have a textarea in the backend script that I would like to convert into a TinyMCE WYSIWYG editable field. Is there a method to achieve this? The following code snippet is not yielding the desired re ...

What is the best way to link options from a select directive with a different array?

Update: the working jsfiddle can be found here: http://jsfiddle.net/robertyoung/jwTU2/9/ I'm in the process of developing a webpage/app using AngularJS. The specific functionality I aim to achieve involves allowing users to add a row to the timecard ...

Looking for guidance on restructuring a JSON object?

As I prepare to restructure a vast amount of JSON Object data for an upcoming summer class assignment, I am faced with the challenge of converting it into a more suitable format. Unfortunately, the current state of the data does not align with my requireme ...

Combining two arrays in JavaScript and saving the result as an XLS file

I have a question that I couldn't find an answer to. I need to merge two arrays and export them to an Excel file using only JavaScript/jQuery. Let's say we have two arrays: Array 1 : ["Item 1", "Item 2"] Array 2 : ["Item 3", "Item 4"] When the ...

Is It Possible to Determine If a Checkbox Has Been Checked?

Here's what I have now: I have a checkbox that I need to verify if it's selected. <input type="checkbox" name="person_info_check" value="0" &nbps>Read and agree!</input> However, the method I found online to verify the checkbox ...

JQuery Ajax: The loaded content flickers into view, revealing old content momentarily

Having an issue with my ajax code. I am working on a project that involves some links and a content area. The idea is that when a user clicks on a link, the content area should be hidden, load new data, and then show the updated content. However, I have no ...

What is the method for populating a dropdown using ajax in the Jade template engine?

Looking to dynamically populate a dropdown based on the selection of another dropdown. Here's the code snippet: script. var b_name = []; function jsFunction() { var client = document.getElementById('drop_client'); var c_name = cli ...