Jnr
Jnr

Reputation: 1664

Can you transform the result of switchMap observable to get its value and not the observable itself?

I'm trying to create an in-memory singleton that holds the current vendor a person is browsing on.

A guard is used on all specific routes to catch the parameter:

canActivate(
  route: ActivatedRouteSnapshot,
  state: RouterStateSnapshot): Observable<boolean | UrlTree> {

  let currentUrl = this._router.url;
  const param = route.params['vendname'];

  return this._vendorService.getByName(param).pipe(map(a => {
    if (a == null) {
      this._snackBarService.open('Vendor not found', 'x', { duration: 5000 });
      return this._router.parseUrl(currentUrl);
    }
    return true;
  }));
}

A service is used to get the vendor by name. If it exists in-memory return it. If it doesn't, get it from the server first.

set vendor(value: IUser) {
  this._vendor.next(value);
}

get vendor$(): Observable<IUser> {
  return this._vendor.asObservable();
}

getByName(name: string): Observable<IUser> {
  const result = this.vendor$.pipe(map(v => {
    if (v != null && v.displayName == name) {
      return v;
    }
    else {
      return this.Get<IUser>(`api/vendor/${name}`).pipe(switchMap(v => {
        this.vendor = v;
        return of(v)
        // ...
      }));
    }
  }))
  return result;
}

The problem is I need to check vendor$ for its value which returns an Obervable<IUser> but the switchMap also returns an Obervable<IUser>, causing the result to be Observable<Observable<IUser>>. How can I make the result return a single User Observable?

Upvotes: 0

Views: 2432

Answers (3)

Jnr
Jnr

Reputation: 1664

I'm adding another solution that worked.

Seeing as though the canActivate returns a Promise as well, you can also use the trusty old async await method.

Note: the toPromise is now lastValueFrom in rxjs 7.

async getByVendorName(name: string): Promise<IUser> {
  const v:IUser = await new Promise(resolve => { 
    this.vendor$.subscribe(a => {
      if (a != null && a.displayName.toLocaleLowerCase() == name) {
        resolve(a);
      }
    });
    resolve(null);
  });

  if (v == null) {
    return this.Get<IUser>(`api/vendor/${name}`).pipe(switchMap(v => {
      this.vendor = v;
      return of(v);
    })).toPromise();

  }
  return Promise.resolve(v);
}

Upvotes: 0

Joshua McCarthy
Joshua McCarthy

Reputation: 1852

I believe you are misunderstanding the use case of switchMap() becuase it's not the cause of your Observable<Observable<IUser>>. Your first map() operator inside getByName() is either returning a value of type IUser (when true) or an observable of IUser (when false). So getByName() returns either Observable<IUser> or Observable<Observable<IUser>>.

However, if you want to try to utilize replaying in-memory values from an observable, then I would recommend shareReplay(). Also, here's a recommended pattern for such a case.

private vendorName = new Subject<string>();

public vendor$ = this.vendorName.pipe(
  distinctUntilChanged(),
  switchMap(name=>this.Get<IUser>(`api/vendor/${name}`)),
  shareReplay(1)
)

public getByName(name:string){
  this.vendorName.next(name);
}

Then in the guard file.

canActivate(
  // ...
){
  let currentUrl = this._router.url;
  const param = route.params['vendname'];

  this._vendorService.getByName(param);

  return this._vendorService.vendor$.pipe(
    map(vendor=>{
      if(!vendor){
        this._snackBarService.open('Vendor not found', 'x', { duration: 5000 });
        return this._router.parseUrl(currentUrl);
      }
      return true;
    })
  );

With this setup, your component(s) & guard can subscribe to vendor$ to get the IUser data it needs. When you have the name to get the vendor, you can invoke getByName(). This will trigger the following steps:

  1. The userName subject will emit the new name.
  2. The vendor$ observable (subscribed to userName) will take that name, then switchMap to get the value from the inner observable (the Get method)
  3. The shareReplay(1) ensures any subscription to vendor$ (current or future) will get the last (1) emitted value from memory.

If you need to update the vendor name, just invoke getByName() with your new name and everything subscribed to vendor$ will automatically get the updated value.

Upvotes: 1

Fateh Mohamed
Fateh Mohamed

Reputation: 21367

related to what I see vendor$ is a stream of IUser, here is a refactoring of your code using iif rxjs operator to subscribe to the first or the second observable based on a your condition

 this.vendor$.pipe(
    mergeMap(v => iif(
      () => v && v.displayName == name, of(v), this.Get<IUser>(`url`).pipe(tap(v => this.vendor = v))
     )
    )
  )

Upvotes: 0

Related Questions