0

I am relatively new to rxjs and angular.

I have a projectRev that I need to retrieve, then I want to retrieve its parent. The API dictates two calls...

I have read a lot of SO posts about queuing Observables but they are all trivial and I am getting lost converting the trivial example into something that applies to me.

Here is something that does work...

Once the first observable is done, call the second one. Simple?

My question: is there a technical reason this approach is flawed. Again... it works. At least on a dev machine that isn't stressed out....

getItems() {
  console.log('get project rev');
  let prId = this.activatedRoute.snapshot.params.id;
  console.log(prId);

  this.projrevSvc.getItemById(prId).subscribe(
    (data) => {
      console.log('this was fetched :  ' + data);
      this.myItem = data;
      this.getProj();

    });
} //getItems

getProj() {
  console.log('pr.pid ' + this.myItem.ProjectId);
  this.projectSvc.getItemById(this.myItem.ProjectId).subscribe(
    (data) => {
      console.log('this project was fetched :  ' + data);
      this.myProject = data[0];
      console.log('prid ' + this.myItem.id)
      console.log(this.myProject);
    });
}
Adrian Mole
  • 49,934
  • 160
  • 51
  • 83
greg
  • 1,673
  • 1
  • 17
  • 30
  • This question poses a real question about a very difficult concept and got 2 very good answers. Yes, there was in fact several technical reasons my initial approach was not reasonable. downvote all you want. Frosty and Bizzybob both provided facts. – greg Oct 30 '21 at 04:34

4 Answers4

3

Your code works. No one can deny that it will work. If the route were to fire often, you would have issues, in that if the route changed and the first call to getProj returned AFTER the second call to getProj, then you would have stale data. So, while it works, it's not fail proof.

The following uses a switchMap so that follow up calls get cancelled and we never risk having stale data.

this.projrevSvc.getItemById(prId)
  .pipe(
    switchMap(item => {
      return this.projectSvc.getItemById(item.ProjectId)
        .pipe(
          map(project => ([item, project])
        );
    })
  ).subscribe(([item, project]) => console.log(item, project))

AND THEN... you should make the stream fully reactive by consuming the params.id as an observable... see here:

this.activatedRoute.params
  .pipe(
    pluck('id'), 
    switchMap(id => this.projrevSvc.getItemById(id))
    switchMap(item => {
      return this.projectSvc.getItemById(item.ProjectId)
        .pipe(
          map(project => ([item, project])
        );
    })
  ).subscribe(([item, project]) => console.log(item, project))

Then feel good because you wrote some fully reactive code that reacts to the changes from the state in the router. Pretty rad.

frosty
  • 21,036
  • 7
  • 52
  • 74
  • 1
    Updated with a better, more reactive, response. Check out the second option. – frosty Oct 22 '21 at 03:57
  • This looks like exactly what I was fishing for, thank you. I am getting better at reading rxjs code, still a ways to go ... where do i map the projRev to this.myItem and the returned project to this.myProject? – greg Oct 22 '21 at 14:16
  • i have read the article @luisenricke suggested. Am i right in thinking i should be doing concatMap instead of switchMap. The issue at hand is I do not know the projectId until I get the projectRev object back, so concatMap seems appropriate? – greg Oct 22 '21 at 14:46
  • this is a very good answer. BizzyBob went into quit a bit more depth and ultimately i was able to get it done. Thank you Frosty. – greg Oct 24 '21 at 19:50
1

You need to check this article for more information https://blog.angular-university.io/rxjs-higher-order-mapping/

But in short answer, you can use something like that

getElements() {
  let prId = this.activatedRoute.snapshot.params.id;
  this.projrevSvc.getItemById(prId)
    .pipe(switchMap(response => this.projectSvc.getItemById(response.ProjectId)))
    .subscribe((nestedReponse) => this.myItem = nestedResponse);
}
luisenricke
  • 159
  • 10
  • 2
    This is good, but he loses pointer to the response from the first call. Only gets returned the second object. – frosty Oct 22 '21 at 03:41
  • i have been looking for some good resources on rxjs. Ill definitely take a read on this one. thank you – greg Oct 22 '21 at 14:24
1

is there a TECHNICAL reason this approach is flawed

Yes. As @frosty points out, you could run into a race condition when getItems() is executed multiple times, since you are storing data "outside of the stream", the state of this.myItem is dependent on the order in which your http requests return.

While this may work most of the time, it is not completely deterministic.

I am getting lost converting the trivial example into something that applies to me

I get it. RxJS is hard... at first :-)

One thing that helped me a lot in becoming proficient is to realize that:

  1. Observables on their own, are pretty boring
  2. RxJS makes working with them worthwhile
  • this is because there are so many operators and static functions that allow you easily create observable sources with well defined behavior.
  1. There are essentially 2 features of an observable: what and when
  • what is the shape of data it emits ?
  • when will it emit this data ?
  1. You can break observables down into smaller parts, which makes understanding and debugging much easier!

Let's take your initial example code (note: for sake of clarity for future readers I've renamed projectRev to item):

export class SomeComponent  {
  
  public myItem    : Item;
  public myProject : Project;

  ngOnInit() {
    this.getItem();
  }

  getItem() {
    let itemId = this.activatedRoute.snapshot.params.id;

    this.itemSvc.getItemById(itemId).subscribe(
      data => {
        this.myItem = data;
        this.getProject();
      }
    );
  }

  getProject() {
    this.projectSvc.getProjectById(this.myItem.ProjectId).subscribe(
      data => this.myProject = data
    );
  }

}

Let's design a single observable that emits exactly the data you want, exactly when you want it!

Thinking of this ahead of time makes life much easier.

For the sake of example, let's say you want to emit an Item with its parent Project attached. So,

  • what : Item object with parent Project appended
  • when : should emit whenever the source item is changed (id is different)

To accomplish this, we can define all the individual parts as separate observables. Angular provides the route params as an observable, so rather than using .snapshot which represents the state at one moment in time, let's define an itemId$ observable that will emit when the param changes:

this.itemId$ = this.activatedRoute.params.pipe(pluck('id'));

Let's also define myItem as observable. We would like myItem$ to emit the current Item (what), whenever the id route parm changes (when):

this.myItem$ = this.itemId$.pipe(
  switchMap(itemId => this.itemSvc.getItemById(itemId))
);

At first, switchMap may seem confusing (I know it was for me). Here's what it does:

  • it internally subscribes to an observable source and emits its emissions
  • each time it receives a new emission, it will stop listening to the previous source and subscribe to a new source
  • in your case, we provided a function that takes the received emission from itemId$ and returns an observable. This observable is the call to this.itemSvc.getItemsById()

So, hopefully you can see that whenever itemId$ emits an id, myItem$ will emit the result of itemSvc.getItemById(), which is the Item object.

Notice, there is no subscription (this is handled internally by switchMap). Notice there is no need to stash the result in a separate local variable this.myItem, which was the cause of your possible race condition.

Next, let's define an observable that emits our Item with an additional project property (what) whenever a new Item is emitted (when):

For the sake of verbosity:

this.myItemWithProject$ = this.myItem$.pipe(
  switchMap(item => this.projectSvc.getProjectById(item.ProjectId).pipe(
    map(project => ({ ...item, project }))
  ))
);

Here we defined myItemWithProject$ as an observable that begins whenever myItem$ emits, then used our new friend switchMap to make a call to get the parent Project. We then use map to simply return a copy of the Item object with an additional project property.

Here's a StackBlitz that shows this altogether.


Maybe you don't want a single combined object, you could obviously shape the data any way you want, maybe a single ViewModel object that has item and project properties. This is actually pretty common in Angular: ​

combineLatest is a great operator to handle this for you:

public vm$ : Observable<ViewModel> = combineLatest({ 
 ​item    : this.myItem$, 
 ​project : this.myProject$}
);

This approach allows you to use a single observable in your template and a single async pipe to unwrap it:

<div *ngIf ="vm$ | async as vm">

  <h2> My Item </h2>
  <p> {{ vm.item | json }} </p>

  <h2> My Project </h2>
  <p> {{ vm.project | json }} </p>

</div>

As your component becomes more complex, you can simply add more sources to the vm$:

public vm$ : Observable<ViewModel> = combineLatest({ 
 ​item    : this.myItem$, 
 ​project : this.myProject$,
 source1 : this.source1$,
 source2 : this.source2$
});

StackBlitz #2

Keeping it "observable all the way" can make things really concise and tidy. But, it requires that you understand what the operators are actually doing (the what and when).

It's usually not necessary to stash data outside of the observable stream. I find that when we reach for that as a solution, it's because we don't yet fully understand all of the operators provided to us by rxjs.


Am i right in thinking i should be doing concatMap instead of switchMap

The difference between the higher order mapping operators only comes into play when they receive more than one emission. This is because they all subscribe to inner sources and emit their emissions. The difference is the strategy they use when a new emission is received before the current source completes:

  • switchMap "switches" sources to only emit from the most recent source.

  • exhaustMap will ignore new emissions until the current source completes, so it only emits from the first source.

  • mergeMap will emit from all sources old and new.

  • concatMap is really just a special case of mergeMap where it will only allow one concurrent source at a time, but will eventually emit from all sources

So, in your case, I think switchMap is appropriate, because if the id changes, you no longer care about listening to emission about object with old ids.

BizzyBob
  • 12,309
  • 4
  • 27
  • 51
  • 1
    trying to work through this. A: this must have taken hours... thank you. B: Does $ declare a variable as an observable? Cant find an answer to this... – greg Oct 24 '21 at 18:45
  • so close.... the only thing I am having troubles w/ is the public vm$ : Observable = combineLatest({ ​item : this.myItem$, ​project : this.myProject$, }); It says it cant find ViewModel. Do I need to import that? Trying to find the component namespace, i ran into an article describing the approach, but i feel i am chasing the wrong thing... – greg Oct 24 '21 at 20:21
  • The `$` suffix on the variable name is just a convention to indicate it’s an observable; it does not actually make it an observable. – BizzyBob Oct 25 '21 at 03:40
  • You would need to create the ViewModel interface yourself. In the StackBlitz, I defined it at the top of the component.ts file. Also, you don’t technically need it, you could just remove the type annotation on the `vm$` if you don’t want to formalize the interface. – BizzyBob Oct 25 '21 at 03:43
  • still having issues. Is it possible to DM you on this site? – greg Oct 25 '21 at 15:45
  • What are the issues? – BizzyBob Oct 27 '21 at 11:05
  • i was still about 4 errors deep when i sent you that. was going to try and see if i could get a short zoom consult. Im unclear as to if that is frowned upon here. Please see my answer below if you are interested... should look pretty familiar to you! thanks a ton! – greg Oct 27 '21 at 15:27
0

alright. much thanks to bizzyBob for putting me on the track This took me 5 days and 3 SO to figure out. I gained a lot of clarity on several concepts trying to hammer this out.

I hope this might help someone else ... Feel free to comment if something could be better...

this example is assuming your service works... here is the projectRev method, the project method is nearly identical

   getItemById(id: string): Observable< ProjectRev> {
     console.log('inside proj rev getItemByid..');
       const url = `${this.serviceUrl}/${id}`;
       return this.http.get< ProjectRev>(url, httpOptions)
           .pipe(
             catchError(err =>
               this.handleError(`getItemById=${id}`, err))
       );

   }

Here is a somewhat redacted listing for my component

    import { Component, OnInit, AfterViewInit, ElementRef, ViewChild } from '@angular/core';

    import { Location } from '@angular/common';
    import { ChangeDetectorRef } from '@angular/core';
    import { Observable,forkJoin, combineLatest } from 'rxjs';
    import { distinctUntilKeyChanged, pluck
          , switchMap ,tap, map} from 'rxjs/operators';
    import { ActivatedRoute } from '@angular/router';
    
    import { Project } from '../project';
    import { ProjectRev} from '../project-rev';
    import { ProjectService } from '../services/project.service';
    import { ProjectRevService } from '../services/project-rev.service';
    
    // this is the one thing I could not get to work. 
    //i could never get the type of the combineLatest() function to return a VM
    interface  VM{
        projectRev : ProjectRev;
        project : Project;
    }
    
    @Component({
      selector: 'app-project-rev-detail',
      templateUrl: './project-rev-detail.component.html',
      styleUrls: ['./project-rev-detail.component.css']
    })
    export class ProjectRevDetailComponent implements OnInit  {
       
       //THESE ARE ALL **OBSERVABLES**
       //One of the hard lessons i learned doing this is you have to be
       //cognizant of  types. You have class types you designed, then 
       // OBSERVABLES, and 
       //  SUBSCRIPTIONS. obviously a<>b<>c , but its easy to overlook
       // I was using ATOM at the start of this exercise.  VSCODE does a 
       // much better job pointing out TYPE mismatches. 
       private projectRevId$ : Observable<string>;
       private myItem$ : Observable<ProjectRev>;
       private myProject$ : Observable<Project>;
    
        //note that this is of type any. 
       public vm$ : Observable<any>;
     
      constructor( private projrevSvc : ProjectRevService
             , private projectSvc : ProjectService
              , private activatedRoute: ActivatedRoute
           ) {
              
            //you are not really supposed to put anything here, if you believe the internet... 
            
           }
    


      //be sure to indicate that your class implements onInit.
      // you also have to import it from '@angular/core'
      ngOnInit(): void {
    

          /*
          This is where the magic happens. 
          We are going to assign the 4 observables
          Except for the first one, each one does a pipe() off the previous

          Couple things i didnt realize about pipe

> A Pipeable Operator is essentially a pure function which takes one
> Observable as input and generates another Observable as output.
> Subscribing to the output Observable will also subscribe to the input
> Observable.
https://rxjs.dev/guide/operators

          pipe() is called on an observable, its a method on an observable
          pipe() returns a NEW observable, it does not change the input observable
          pipe() has a parm list for its signature. Thats why the syntax looks odd. 
       in parm, you can call any number of map operators

          Each operator looks something like this: 
                     map( (proj) =>  { console.log( proj) } )

          the parameter proj can be named whatever. 
          I like to read it this way:  I got back 'proj' , so i FatArrow  <<my stuff>>
          Fat Arrow can be followed by a single statement, no semicolon necessary
          Fat Arrow can also be followed by curlies containing multiple statements. 
              Semicolons are required here.
         
           ** Make sure you put the returned value(s) in parens before the FatArrow
           ** Make sure you get the closing paren on the operator. Its best to always count parens on each line. They all have to be enclosed inside the pipe() parens.  Its very , very easy to mess it up. good luck!


          If you want to test your chain 1 at a time, be sure and subscribe to the last one you are looking at. 
          I have left my subscribes commented, but in place as an example 
 

*/




        //**
        //** Get the id from the active route
        //**
        this.projectRevId$ = this.activatedRoute.params.pipe(
            pluck('id')
            ,tap( id => {  console.log('pRevId',id);})
            );
            //).subscribe( (id)=> console.log('the url id is :',id));
       



        //**
        //** Get the item (projectRev) using the ID from the URL
        //**
        //**  switchMap
        //**   https://rxjs.dev/api/operators/switchMap
        //**   still not sure switchMap is the best operator, but its working
        //**
            this.myItem$ = this.projectRevId$.pipe(
                tap( item => {  console.log('inside projrev pipe',item);})
                ,   switchMap( (itemId : string) => this.projrevSvc.getItemById(  itemId   ))
                , tap( item => console.log('rev retrieved',item))
            );
            // ).subscribe( (pr) => console.log('projecRev=',pr));
    



        //**
        //** Get the project using the just retrieved projectRev.projectid value
        //** https://rxjs.dev/api/operators/distinctUntilKeyChanged - nice....
        //** notice i use 'pr' in switchMap and 'item' in tap
        //**  i was trying to test what matters syntax wise...
        //**  readability wise, consistency and 'obvious' is better
            this.myProject$ = this.myItem$.pipe(
                tap( (prev : ProjectRev) =>{  console.log('inside getProj pipe');})
                ,distinctUntilKeyChanged('ProjectId')
                ,switchMap((pr) => 
                        this.projectSvc.getItemById(pr.ProjectId))
                , tap( item => console.log('proj retrieved',item))
             );  
            //  ).subscribe( (p ) => console.log('projec =',p ));
    


        //**
        //** And finally, combine them retrieved 
        //** https://rxjs.dev/api/index/function/combineLatest
        //**
        //** NOTE: the 2 OBSERVABLES are passed in as a single array 
        //**
        //** in the map(), i used better names, but note they dont match 
        //** what I used before...
        //** the map is from this SO
        //**https://stackoverflow.com/questions/52942356/defining-the-result-of-combinelatest-once
        //**  this map is important when we move on to the HTML


            this.vm$ = combineLatest(
                  [ 
                  this.myItem$,
                  this.myProject$
                  ]
              ).pipe(
                 map( ([projectRev,project ]) => ({projectRev, project}))  
              );
              
              //we DO NOT need a SUBSCRIBE because we 
              //  are using the async pipe on the outer 
              //  div in the HTML and that does a subscribe
              // IF YOU SUBSCRIBE HERE, YOU WILL DO EVERYTHING TWICE

      }
    }

And finally, we can use this in the HTML template This is a great article on getting the ngIf right... https://blog.angular-university.io/angular-reactive-templates/

  • ummm, async does not have an H at the end.

  • ngIf has a capital I

  • vm$ needs to be an observable

  • vm1 does not need to be defined in your class

  • the async pipe does a subscribe on the named Observable. Easy to forget...

  • the div w/ {{vm1}} will show you what vm1 actually is. It may not be what you think it is. Nice debugging to figure out why nothing is showing up.

       <ng-template #loading>
         <div class="col-xs-8" >loading</div>
     </ng-template>  
     <div class="col-xs-8"  *ngIf="vm$ | async as vm1; else loading">
       <div> {{vm1 }} </div>
       <div ><a href="#" >{{  vm1.project.Name     }} </a> >
               <a href="#"> Rev. {{ vm1.projectRev.RevisionNumber }} - {{vm1.projectRev.Name}} </a>
         </div>
     </div>
    

voila - I have breadcrumbs being displayed in a completely async manner!!!! I know that seems crazy, but now I have all the data I need to complete the rest of the page. And even if I do need to mix in a 3rd call, i feel i have learned the technique well enough it would take a small handful of minutes to mix in another data source.

Rereading the 2 other answers... Frosty chained them all together, which improves concision

BizzyBob actually defined and initialized all the observables in the declarations section.

there are so many ways to accomplish the same thing. I think thats part of what it makes it difficult to synthesize a working solution by sampling different SO answers.

greg
  • 1,673
  • 1
  • 17
  • 30