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.subscription = timer(0, 120 * 1000).subscribe(() => {
        this.shopService.getPreferencesAsObservable().subscribe(preferences => {
            if(preferences) {
                this.getInitialPendingSlotsOrders();
                this.getInitialPendingNoSlotsOrders();
            }
        });
    });
}

getInitialPendingSlotsOrders(){
    this.apiService.fetchShopOrders("status=PENDING&only_slots=true").subscribe(orders=> {
        /* do stuff with orders */
        /* may need to retrieve additional data */
        if(orders.next_page){
            this.getNextSlotsSetOfPendingOrders();
        }
    });
}

getInitialPendingNoSlotsOrders(){
    this.apiService.fetchShopOrders("status=PENDING").subscribe(orders=> {
        /* do stuff with orders */
        /* may need to retrieve additional data */
        if(orders.next_page){
            this.getNextNoSlotsSetOfPendingOrders();
        }
    });
}

getNextSlotsSetOfPendingOrders() { 
    this.apiService.fetchNextSetOfOrders(this.nextSlotsUrl).subscribe(nextSetOfOrders => {
        /* do stuff with next set of orders */
    })
}

getNextNoSlotsSetOfPendingOrders() { 
    this.apiService.fetchNextSetOfOrders(this.nextNoSlotsUrl).subscribe(nextSetOfOrders => {
        /* do stuff with next set of orders */
    })
}

Although I believed this approach would be effective, there seems to be an issue with extra API calls being made in some cases. I suspect this could be due to chaining observables. How can I improve and optimize this setup?

Appreciate your assistance.

Answer №1

Having multiple nested subscriptions can result in open subscriptions that may not be closed. It is recommended to utilize RxJS operators to limit it to a single subscription.

If you need to trigger two requests simultaneously, consider using the RxJS forkJoin function.

For more information, check out this link.

To summarize:

  • switchMap: Maps from one observable to another
  • filter: Continues the operator chain based on a condition
  • forkJoin: Combines and triggers multiple observables in parallel

Here's an example to try:

ngOnInit(): void {
  this.subscription = timer(0, 120 * 1000).pipe(
    switchMap(() => this.shopService.getPreferencesAsObservable()),
    filter(preferences => !!preferences),
    switchMap(() => 
      forkJoin({
        slots: getInitialPendingOrders(true),
        noSlots: getInitialPendingOrders(false)
      })
    )
  ).subscribe({
    next: ({ slots, noSlots }) => {
      // handle orders from `slots` and `noSlots`
    },
    error: (error: any) => {
      // handle error
    }
  });
}

getInitialPendingOrders(slots: boolean): Observable<any> {
  return this.apiService.fetchShopOrders("status=PENDING" + (slots ? "&only_slots=true" : ''));
}

Update

It is advisable to return the observable and subscribe only when the response is needed. You can use switchMap for each argument of forkJoin and return an observable conditionally. Use RxJS constant EMPTY to emit results from forkJoin when nothing needs to be returned. Note that forkJoin will emit only when all source observables complete.

ngOnInit(): void {
  this.subscription = timer(0, 120 * 1000).pipe(
    switchMap(() => this.shopService.getPreferencesAsObservable()),
    filter(preferences => !!preferences),
    switchMap(() => 
      forkJoin({
        slots: getInitialPendingOrders(true).pipe(
          switchMap((orders: any) => {
            // perform operations with orders
            return orders.next_page ? this.getNextSlotsSetOfPendingOrders() : EMPTY;
          })
        ),
        noSlots: getInitialPendingOrders(false).pipe(
          switchMap((orders: any) => {
            // perform operations with orders
            return orders.next_page ? this.getNextNoSlotsSetOfPendingOrders() : EMPTY;
          })
        )
      })
    )
  ).subscribe({
    next: ({ slots, noSlots }) => {
      // handle next set of orders from `slots` and `noSlots`
    },
    error: (error: any) => {
      // handle error
    }
  });
}

getInitialPendingOrders(slots: boolean): Observable<any> {
  return this.apiService.fetchShopOrders("status=PENDING" + (!!slots ? "&only_slots=true" : ''));
}

getNextSlotsSetOfPendingOrders(): Observable<any> { 
  return this.apiService.fetchNextSetOfOrders(this.nextSlotsUrl);
}

getNextNoSlotsSetOfPendingOrders(): Observable<any> { 
  return this.apiService.fetchNextSetOfOrders(this.nextNoSlotsUrl);
}

Answer №2

It's generally advised not to use the subscribe function inside another subscribe (although there are exceptions).

I suggest taking a look at Is it good way to call subscribe inside subscribe? and familiarizing yourself with various operators such as forkJoin, mergeMap, etc., that RxJs offers for combining observables in different ways based on your specific needs.

Answer №3

There are numerous ways to connect observables, making this a versatile question. For a comprehensive list of combination operators, check out the Combination operators on Learn RxJs.

In my experience with rxjs, I've found it beneficial to break down complex processes into const variables or readonly properties, and then chain them together using a separate observable as needed. This approach helps in maintaining clarity and structure as a project expands. Additionally, it eliminates redundant method calls by executing streams only upon subscription.

In the scenario provided below, after each timer interval, the stream transitions to the getPreferencesAsObservable() method, which likely emits once. Upon emission, the filter() is applied based on valid preferences. The results from both observables retrieving shop orders are combined using forkJoin().

I faced a choice between using concatMap and switchMap. The former ensured that no emissions were skipped even if subsequent emissions differed. On the other hand, with switchMap, any incomplete previous emission would be terminated before the next one. The decision ultimately depends on the desired behavior for the application.

readonly noSlotsProcess$ = 
  this.apiService.fetchShopOrders("status=PENDING").pipe(
    tap((x) => /* do stuff */)
  );
readonly slotsProcess$ = 
  this.apiService.fetchShopOrders("status=PENDING&only_slots=true").pipe(
    tap((x) => /* do stuff */)
  );
readonly refresh$ = timer(0, 120 * 1000).pipe(
  concatMap(() => this.shopService.getPreferencesAsObservable()),
  filter((preferences => !!preferences),
  switchMap(() => { forkJoin( noSlots: this.noSlotsProcess$, slots: this.slotsProcess$ }))
);
ngOnInit(): void {
    this.subscription = this.refresh$.subscribe();
}

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

The selected value of the PrimeNG p-checkbox cannot be determined using a function when binding to [ngModel]

These are the rows of my custom p-table <tr> <td>{{user.userName}}</td> <td>{{use.userSurname}}</td> <td *ngFor="let group of groups"><p-checkbox [(ngModel)]="showVal ...

Angular - Implementing a debounce feature for event handling

Currently, I am utilizing the mouseenter and mouseleave events to control the opening and closing of my sidenav within the app. However, I have encountered issues because when hovering over the container quickly, these events are triggered multiple times ...

Error message in Angular 2: "__generator is not recognized"

I've been working on intercepting outgoing HTTP requests in Angular 2 in order to generate a token from the request body and attach it to the header of each post request. Below is the code snippet that I've implemented. Initially, I encountered ...

Executing a PHP function within a Laravel controller using Ajax

In my Laravel project, I have a Controller named Clientecontroller that is working perfectly. It contains a method called listar() which retrieves client information. public function listar(Cliente $cliente) { $clientes = DB::table('clientes' ...

Changing icons within an ngFor loop in Angular 2

Looking for a solution: How can I toggle icons using ngFor? Situation: I am using *ngFor to iterate through an array and display category names. When a day is clicked, I want to open an accordion and show category details (which I have already managed). O ...

Merging RXJS observable outputs into a single result

In my database, I have two nodes set up: users: {user1: {uid: 'user1', name: "John"}, user2: {uid: 'user2', name: "Mario"}} homework: {user1: {homeworkAnswer: "Sample answer"}} Some users may or may not have homework assigned to them. ...

The JSON object cannot be assigned to the IntrinsicAttributes and customType data types

Currently, I'm facing a challenge with reading JSON data into an array of type pinImage. My goal is to loop/map through my pinImage objects and pass each one to a child component called PinCard, which is specifically designed to accept an object of ty ...

The ngFor directive in Angular2 consistently collapses the identical <tr> element

Greetings, I am a newcomer to the world of web development. Recently, I used the *ngFor directive in Angular to generate multiple rows with collapsible details. However, when I click on a row, it always collapses the same div, instead of the corresponding ...

Discovering specific values for an ID using API calls in Angular (Implementing CRUD Operations in Angular with API Integration)

My current project involves CRUD operations in Angular utilizing the API created in Laravel. I have successfully added and fetched values, but encountered an issue when attempting to update values using their respective IDs. This snippet is from my app.co ...

Issue: Module '@nrwl/workspace/src/utilities/perf-logging' not found

I attempted to run an Angular project using nxMonorepo and made sure to install all the necessary node modules. However, when I tried running the command "nx migrate --run-migrations" in my directory PS C:\Users\Dell\Desktop\MEANAPP&bso ...

Customizing the design of the datepicker in Angular and then passing the formatted date to a

I need to send a date to the node for storage, but I am receiving it in this format 2022-04-26T18:30:00.000Z. I want to change it to 26-04-2022 Angular HTML Code <mat-form-field color="accent" appearance="fill"> <mat-label&g ...

angular change digits to Persian script

I am trying to convert English numbers to Persian numbers in angular 4: persianNumbers = ["۰", "۱", "۲", "۳", "۴", "۵", "۶", "۷", "۸", "۹"]; engli ...

Deciphering a mysterious message in Typescript while defining a function for one of my tasks

Currently, I am working with a stack that includes React, TypeScript, and Redux. Unfortunately, I have encountered an issue while using an interface for one of my actions. The error message I received is quite cryptic: Duplicate identifier 'number&apo ...

The NX Monorepo housing a variety of applications with unique design themes all utilizing a single, comprehensive UI component

We are currently working on a design system for a NX monorepo that has the potential to host multiple apps (built using Next.js), all of which will share a common component library. While each app requires its own unique theme, the UI components in the lib ...

Customizing the appearance of mat-form-field when it is not in focus and has text in the mat-input field

Is there a way to maintain the style of an input field when it is focused and contains text, without applying the out-of-focus styles? Currently, I want the following styles to remain when the field is out of focus with text: https://i.stack.imgur.com/bz ...

The role of callback functions in TypeScript

As I embark on my journey with Angular 2 and TypeScript, one concept that has me a bit puzzled is how to implement callback functions. I understand that this might be a basic question, but when I look at this typical JavaScript code: someOnject.doSomethin ...

Adding a second interface to a Prop in Typescript React: a step-by-step guide

import { ReactNode, DetailedHTMLProps, FormHTMLAttributes } from "react"; import { FieldValues, SubmitHandler, useForm, UseFormReturn, } from "react-hook-form"; // I am looking to incorporate the DetailedHTMLProps<FormHTMLAt ...

Icon appearing outside the button instead of inside

Here's a button I'm working with: <button class="glyphicon glyphicon-option-horizontal" (click)="myFuncion()"></button> Recently, I updated my bootstrap library from version 3 to 4.2.1 and now the icon is no longer visible. I' ...

Automatically generated error notifications for Express-Validator

I am looking to implement an Express API and incorporate request input validation using express-validator. Here is my current validation middleware: protected validate = async (request: Request, response: Response, next: NextFunction): Promise<void> ...

Issue with TagCloud functionality when deployed on Vercel platform

Everything functions perfectly on my local machine, but once deployed to Vercel, the display disappears. I've double-checked the text, container, and TagCloud - all showing the correct responses. I even tried uninstalling and reinstalling TagCloud wit ...