angularrxjsobservableangular-routerdeclarative-programming

How can I get an object by ID using observables (declarative pattern)


I am attempting to make my application more reactive/declarative vs procedural & having a really hard time trying to grasp not passing arguments to/from my service.

My url looks like this: /settings/organizations/26ee00d1-9fd8-4e38-a195-024c11ae0958

// organization.component.ts


organizationId: string;

...
constructor(private readonly activatedRoute: ActivatedRoute) {
    //...
}

ngOnInit(): void {
    this.organizationId = this.activatedRoute.snapshot.paramMap.get('organizationId')!;

    this.fetchOrganization(this.organizationId);
}

...

fetchOrganization(organizationId: string): void {
    this.organization$ = this.organizationService
    .getOrganization(organizationId)
    .pipe(
        catchError((err) => {
           console.log('err: ',err);
           return EMPTY;
        })
    );
}

Here is what my service looks like:

// organization.service.ts

...

getOrganization(organizationId: string): Observable<Organization> {
    return this.httpClient.get<Organization>(
                `${this.baseURL}/v1/organizations/${organizationId}`
    )
    .pipe(catchError((err) => throwError(err)));
}

I've been pouring over this SO thread to think more reactive and not "pass" values around. I am slowly grasping that way of thinking, but I'm really struggling with how to get the organizationId to my service in a declarative way.

For example, this is what my commponent.ts file might look like:

organization$ = this.organizationService.organization$.pipe(
        catchError((err) => {
            this.handleError(err);
            return EMPTY;
        })
    );

Then the service like this:

 organization$ = this.httpClient
        .get<Organization>(
            `${this.baseURL}/v1/organizations/${organizationId}`
        )
        .pipe(catchError((err) => throwError(err)));

If I'm not "passing" the organizationId to my service, how can I get the value of organizationId? Does the routing logic live in the service now?

I've also tried without success trying to make organization$ in my service some sort of Subject or BehaviorSubject. I am just learning some of these techniques, and I understand the basics of Observable/Subjet/BehaviorSubjet etc. I'm really struggling how to get the id the "angular" way.

I think this GitHub code is very similar to what I am trying to accomplish. Specifically this part:

private productSelectedSubject = new BehaviorSubject<number>(0);
productSelectedAction$ = this.productSelectedSubject.asObservable();

//

private organizationSubject = new BehaviorSubject<string>('26ee00d1-9fd8-4e38-a195-024c11ae0958');
organizationId$ = this.organizationSubject.asObservable();

// where/how do I get the actual value of the url segment?

I am struggling how to pick up the url vs. an input change.

EDIT/UPDATE

I failed to mention what my template looks like. I am using the async pipe to subscribe/un-subscribe to my organization$ variable.

<ng-container *ngIf="organization$ | async as organization; else loading">
    <div>{{ organization.name}}</div>
</ng-container>

...

<ng-template #loading>
    <div>Loading...</dv>
</ng-template>

Solution

  • RxJS takes time - I think I struggled for six months to wrap my head around the basics - so you aren't alone.

    In this case I think I would not use the snapshot of the activated route. The advantage of not using the snapshot is that now the component can listen to route changes of whatever variable is in the route.

    Pretending you have the async pipe in your html - which will unsubscribe for you (yay!)

    <div *ngIf="organization$|async as organization; else loading">
    <!-- show org info -->
    {{organization.name}}
    </div>
    <div #loading>
      <some-spinner></some-spinner>
    </div>
    

    I have a habit of assigning this in the onInit - I think to make unit testing just a little easier (create component and set things in beforeEach, then call fixture.detectChanges in my test to fire the ngOnInit)

    Since organization is a stream I usually put the $ at the end of the name to indicate that.

    ngOnInit(): void {
      this.organization$ = this.activatedRoute.paramMap.pipe(
        concatMap(params => {
          const organizationId = params.get('organizationId');
          return this.organizationService.getOrganization(organizationId)
            .pipe(
              catchError((err) => {
                console.log('err: ',err);
                return EMPTY;
              })
            )
        }
      );
    }
    

    The concatMap is important here as it tells us that you expect that activatedRoute.paramMap to be a stream, and when it emits a value we want to call something else that also returns a stream (observable). It also indicates that everything will call in the same order as it was emitted - so if someone was clicking next between different orgs then it would load each org from httpClient before performing the next one.

    Pretending you have access to PluralSight, I learned a lot from watching RxJS in Angular: Reactive Development by Deborah Kurata and I think I had to watch it at least 3 times. Here is a talk at ng conf 2022 that she did and may also help.

    I'm sure there are other good resources out there, too.

    You mention listening to an input change - that indicates something different. For that I like to use a setter for my input when I need to handle additional logic.

    @Input()
    set organizationId(value:string) {
      // do any validation here  
      // add value to the stream
      this.organizationId$.next(value);
    }
    
    organization$: Observable<Organization>;
    private organizationId$ = new Subject<number>();
    
    constructor(organizationService: OrganizationService){
      
      this.organization$ = this.organizationId$.pipe(
        switchMap(organizationId => this.organizationService.getOrganization(organizationId)
          .pipe(
            catchError((err) => {
              console.log('err: ',err);
              return EMPTY;
            })
          )
      }
    )
    }
    

    Here I used switchMap just to show something different. It will cancel any call inside of it when a new value comes to it - then it will only use that newest value. This is handy if you think someone will just start clicking next several times - you won't have to wait on each http call like you would with concatMap.