user11962606
user11962606

Reputation:

How can I define the execution order of subscription to Observables in Angular?

I have got some get-Calls from an API in a service.ts file:

//code
getCars()
{
this.car = this.http.get(car_url)
return this.car;
}

getTires()
{
this.tires = this.http.get(tires_url)
return this.tires;
}

getSeats()
{
this.seats = this.http.get(seats_url)
return this.seats;
}

In the detail.compoenent.ts I filter this data to a selected car and then present it through detail.component.html - detail.compoenent.ts looks like this:

//code
ngOnInit() 
{
this.Service.getCars()
.subscribe(cars => this.car = cars.find(/*code (selecting car with certain car_id*/)

this.Service.getTires()
.subscribe(tires => {this.tires = tires.filter(/*code (selecting all the tires with the selected car_id*/)}

this.Service.getSeats()
.subscribe(seats => {this.seats = seats.filter(/*code (selecting all the seats with the selected car_id*/)}
}

To filter for the tires and seats, getCar() has to be executed first because information of it is needed to filter for the tires and seats. So how do I have to put the code to ensure this.Service.getCars()subscribe(/code/) is executed before this.Service.getTires().subscribe(/code/) and this.Service.getSeats().subscribe(/code/)?

Upvotes: 4

Views: 4926

Answers (3)

Ashish Ranjan
Ashish Ranjan

Reputation: 12960

You can use mergeMap() and subsequently call other observables, you can use the data of the other previous API in another.

this.Service.getCars().pipe(mergeMap((cars) => {
    // cars is available for the other two
    return this.Service.getTires().pipe(mergeMap((tierData) => {
        // tierData is avaibale for getting seats.
        return this.Service.getSeats()
    }))
})).subscribe((seats) => {

})

You can merge the other two in the first pipe itself.

Upvotes: 1

maxime1992
maxime1992

Reputation: 23793

Having nested subscriptions is a big code smell. If you feel the need to have a nested subscription, please search higher order observables like switchMap, concatMap, mergeMap, zip, etc.

I took the time to build a full demo of what you're trying to achieve.

First, let's start by defining our interfaces to have some type safety:

export interface Car {
  id: string;
  seatId: string;
  tyreId: string;
}

export type CarResolved = Omit<Car, "seatId" | "tyreId"> & {
  seat: Seat;
  tyre: Tyre;
};

export interface Tyre {
  id: string;
  diameter: number;
}

export interface Seat {
  id: string;
  width: number;
}

Now that we know what kind of data structure we want, let's build a service and return mock data that you can later on replace with your backend:

@Injectable()
export class ResourcesService {
  public getCars(): Observable<CarResolved[]> {
    return this._getCars().pipe(
      switchMap(cars =>
        forkJoin(
          ...cars.map(car =>
            forkJoin(
              this._getTyreById(car.tyreId),
              this._getSeatById(car.seatId)
            ).pipe(
              map(([tyre, seat]) => ({
                id: car.id,
                seat,
                tyre
              }))
            )
          )
        )
      )
    );
  }

  private _getCars(): Observable<Car[]> {
    return of(mockCars);
  }

  private _getTyreById(id: string): Observable<Tyre> {
    return of(mockTyres.find(tyre => tyre.id === id));
  }

  private _getSeatById(id: string): Observable<Seat> {
    return of(mockSeats.find(seat => seat.id === id));
  }
}

const mockCars: Car[] = [
  { id: "car-1", seatId: "seat-1", tyreId: "tyre-1" },
  { id: "car-2", seatId: "seat-2", tyreId: "tyre-2" },
  { id: "car-3", seatId: "seat-1", tyreId: "tyre-3" }
];

const mockTyres: Tyre[] = [
  { id: "tyre-1", diameter: 80 },
  { id: "tyre-2", diameter: 60 },
  { id: "tyre-3", diameter: 75 }
];

const mockSeats: Seat[] = [
  { id: "seat-1", width: 10 },
  { id: "seat-2", width: 20 },
  { id: "seat-3", width: 30 }
];

If you look at the getCars method, it has absolutely no subscribe. It just returns an observable that someone can later on subscribe to. Everything is done through streams :)

Finally, the easiest part: The view.

@Component({
  selector: "my-app",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"]
})
export class AppComponent {
  public cars$: Observable<CarResolved[]> = this.resourcesService.getCars();

  constructor(private resourcesService: ResourcesService) {}
}

Notice here too: No subscription at all! :)

And the HTML side:

<pre>{{ cars$ | async | json }}</pre>

Our view now shows our array of resolved cars:

[
  {
    "id": "car-1",
    "seat": {
      "id": "seat-1",
      "width": 10
    },
    "tyre": {
      "id": "tyre-1",
      "diameter": 80
    }
  },
  {
    "id": "car-2",
    "seat": {
      "id": "seat-2",
      "width": 20
    },
    "tyre": {
      "id": "tyre-2",
      "diameter": 60
    }
  },
  {
    "id": "car-3",
    "seat": {
      "id": "seat-1",
      "width": 10
    },
    "tyre": {
      "id": "tyre-3",
      "diameter": 75
    }
  }
]

Here's a live demo on stackblitz: https://stackblitz.com/edit/angular-qypary

Upvotes: 0

Santosh Kadam
Santosh Kadam

Reputation: 1352

Simple solution could be make subsequent http requests in call back functions.

ngOnInit() 
{
    this.Service.getCars().subscribe(
      cars => {
           this.car = cars.find();
           this.Service.getTires().subscribe(tires => {
              this.tires = tires.filter();
              this.Service.getSeats().subscribe(
                seats => {this.seats = 
              seats.filter(/*code (selecting all 
                 the seats with the selected car_id*/)}
            }
         }

      });

}

until all data is getting loaded you can show spinner to end user.

Upvotes: -1

Related Questions