Search code examples
rxjsobservablengrx

NGRX, Rxjs: what is the better solution for this case? Observable in Action?


In my application, I have a case as following, I want to know how to do it in a better way:

// the effects function
this.user$ = this.actions$.pipe(
  ofType(UserActions.FETCH_USER),
  switchMap((_) => {
    return userService.getUser().pipe(
      map((res) => {
        return new UserActions.FetchUserSuccess(this.transformUserResData(res));
      }),
      catchError(() => {
        return observableOf(new UserActions.FetchUserFail());
      })
    );
  })
);

this is a simple effect function, the only point need to mention is for the success case, before passing the response data to the FetchUserSuccess function as payload. I need to transform it. And the transform function is as following:

  private transformUserResData(users: User[]): UserInfo[] {
    let groupIDNames: Array<IdNameMap>;
    this.groupStoreService.groupIDNames$.subscribe((res) => {
      groupIDNames = res; // array of {Id: "id", Name: "name"} object
    })
    return users.map((each) => {
      return {
        id: each.id,
        title: each.title,
        category: each.category, 
        groupName: groupIDNames.find(current => current.Id === each.groupID).Name
      };
    })       
  }

as you can see, the original transform data is array of user, and in each user there is a property named gruopID, which simply means the user belongs to which group.

And the transform function's purpose is map groupID to groupName, I want to add the group name information into the user object. Clear target, right?

And tricky point is here, the group id and name mapping data is an Observable. In fact it's the state values selected from NGRX store with store.select method as the this.groupStoreService.groupIDNames$ goes.

So my current solution is posted here, you can see I subscribe to the Observable and get the data as groupIDNames. And use it in the later parsing.

This can work, since the return value this.groupStoreService.groupIDNames$ from store.select is SubjectBehavior type (this point is confirmed in my another post), so the subject callback function is sync.

It worked, but I know it's not best practice to do this. So I want to know how to improve it in another way.

I thought use the following method:

  private transformUserResData(users: User[]): Observable<UserInfo[]> {
    this.groupStoreService.groupIDNames$
       .pipe(map((groupIDNames) => {
          return users.map((each) => {
            return {
              id: each.id,
              title: each.title,
              category: each.category, 
              groupName: groupIDNames.find(current => current.Id === each.groupID).Name
           };
         })  
      }))     
  }

But in this way, the transform function will return Observable, and the Observable will be passed to redux action. I don't know it's best practice for redux.

If any other smarter solution? Thank you


Solution

  • You can update your code a bit more reactively by composing your ngrx store observable with API response like this:

    // the effects function
        this.user$ = this.actions$.pipe(
          ofType(UserActions.FETCH_USER),
          switchMap((_) => {
            return userService.getUser()
                             .pipe(
                                    //lets transform the data
                                    mergeMap((users: User[]) => {
                                      return this.groupStoreService.groupIDNames$
                                                 .pipe(
                                                   map((groupIDNames: Array<IdNameMap>) => {
                                                      return users.map((each) => {
                                                        return {
                                                          id: each.id,
                                                          title: each.title,
                                                          category: each.category, 
                                                          groupName: groupIDNames.find(current => current.Id === each.groupID).Name
                                                        };
                                                      });
                                                   })
                                                 );
                                    }),
                                    map((userInfos: UserInfo[]) => {
                                      return new UserActions.FetchUserSuccess(userInfos);
                                    }),
                                    catchError(() => {
                                      return observableOf(new UserActions.FetchUserFail());
                                    })
                              );
          })
        );
    

    If you want to avoid nesting of the operators, you can also write like this:

    // the effects function
        this.user$ = this.actions$.pipe(
          ofType(UserActions.FETCH_USER),
          switchMap((_) => {
            return userService.getUser()
                             .pipe(
                                    mergeMap((users: User[]) => {
                                      return combineLatest(of(users), this.groupStoreService.groupIDNames$.pipe(take(1)));
                                    }),
                                    map(([users, groupIDNames]) => {
                                      //project [map] your users to userInfo
                                      const userInfos = users.map((each) => {
                                                                    return {
                                                                      id: each.id,
                                                                      title: each.title,
                                                                      category: each.category, 
                                                                      groupName: groupIDNames.find(current => current.Id === each.groupID).Name
                                                                    };
                                                                  });
                                      return new UserActions.FetchUserSuccess(userInfos);
    
                                    }),                                
                                    catchError(() => {
                                      return observableOf(new UserActions.FetchUserFail());
                                    })
                              );
          })
        );