1

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>
Damon
  • 4,151
  • 13
  • 52
  • 108

2 Answers2

2

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.

Wesley Trantham
  • 1,154
  • 6
  • 12
  • I cannot thank you enough! This is such a great explanation. I do have a Pluralsight membership :) Thats where I knew to look through the GitHub examples. She was working with an activity stream -- and I new I needed to focus/translate what she had into a *data* stream. I was so focused on *not* trying to be procedural that I thought passing the ID to the service wasn't necessary. I wasn't sure how else that would of worked though! Thank you, thank you, thank you! – Damon Oct 18 '22 at 11:09
  • +1 for the `set @Input()`. I find that to be a clean way to make component code reactive. Much cleaner than using `ngOnChanges`. Also, I don't think `concatMap` will work in your first example. Since the `paramMap` observable will never complete `concatMap` will never unsubscribe from the first source observable. I think switchMap is the way to go. – BizzyBob Oct 18 '22 at 19:46
1

trying to grasp not passing arguments to/from my service

You can still maintain a reactive / declarative approach and allow passing arguments to your service. Your service method is just fine, taking an 'id' argument and returning an Observable<Organization>.

Example:

Component

private id$ = this.route.paramMap.pipe(
    map(params => params.get('organizationId'))
);

public organization$ = this.id$.pipe(
    switchMap(id => this.fetchOrganization(id))
);

constructor(
    private route: ActivatedRoute,
    private service: OrganizationService
) { }

private fetchOrganization(id: string) {
    return this.service.getOrganization(id).pipe(
        catchError(err => {
            console.log(`Unable to get data for organization ${id}.`);
            return EMPTY;
        })
    );
}

Notice this solution is very close to your original code, with the two differences being:

  • the organizationId param comes from an observable
  • we've defined an organization$ observable that emits the Organization

To address your question:

Does the routing logic live in the service now?

I say NO. It could, but that puts too much responsibility on your service if it has knowledge of the router. You can see above, you don't need to change your service method at all. We declare the organization$ observable to start with the id$, we pipe id emissions into switchMap which will internally subscribe to the observable getOrganization(id) and emit the result (the Organization object). Now, if id$ ever emits a new value, organization$ will emit the new Organization. This is still very much reactive!

I've also tried without success trying to make organization$ in my service some sort of Subject or BehaviorSubject

I've found that in most cases it's preferable to have the service methods take an argument rather than push a value through a Subject. This allows you to call the service with different arguments and have references to different entities:

org1$ = this.service.getOrginization(1);
org2$ = this.service.getOrginization(2);

You lose the ability to do this if the service internally manages a single id. Not saying it's necessarily "bad" to do it that way, it's just less flexible IMO.


Some tips for simplest RxJS code:

  • initialize observables "on the class"; ngOnInit is not usually necessary
  • define "smaller" observables by giving meaningful names to smaller parts (like id$ above)
  • it's fine to pass params to service methods; doing so does not inherently break the reactive paradigm
  • leverage rxjs operators to design an observable that emits exactly the data you want
  • leverage the async pipe in your components to handle subscriptions automatically
BizzyBob
  • 12,309
  • 4
  • 27
  • 51
  • 1
    This is so incredibly helpful. Thank you so much for taking the time to explain this. I know I'm *right* on the edge of that lightbulb moment & this is extremely helpful. – Damon Oct 18 '22 at 22:12