avj
avj

Reputation: 1686

Avoid nested observables

I need to fill in three controls with hierarchic structure: companies, branches, employees. Out of user service I get currently logged in user, then via user's domain name I receive his full data as Employee, then per employee's branchId I obtain branch object, then I request all companies and set control values as currrent employee, current branch, and current branch's company.

I end up with nested tree of subscribers. How can I avoid this and refactor it to be more straightforward?

this.userService.getCurrentlyLoggedInUser().subscribe(
    user => {
        this.user = user;
        this.getEmployee(this.user.domainName).subscribe(
            employees => {
                if (employees.length === 0) {
                    this.isLoading = false;
                    return;
                }
                this.getBranch(employees[0].branchId).subscribe(
                    branches => {
                        if (branches.length === 0) {
                            this.isLoading = false;
                            return;
                        }
                        this.odata.companies().subscribe(
                            companies => {
                                this.setDefaultValues(companies, branches[0], employees[0]);
                                this.isLoading = false;
                            },
                            error => this.isLoading = false
                        );
                    },
                    error => this.isLoading = false
                );
            },
            error => this.isLoading = false
        );
    },
    error => this.isLoading = false
);

Upvotes: 3

Views: 1685

Answers (1)

dotcs
dotcs

Reputation: 2296

To avoid nested subscribe calls you can always map back from a meta branch, such as a stream that has been forked from the main branch through some AJAX call or some other async operation, to a trunk branch by using mergeMap.

See the following code for details. Notice, that there is only one subscribe call to the trunk stream. All other streams are mergeMap'd back into the trunk stream such that no additional subscriptions are required. Consider subscribing to a stream as breaking out of the RxJS context and get back to normal JS. This should only be required for side effects, such as the console.log command.

const initialUserObj = { userId: 0, username: 'admin' };

const getUserGroup = function(id) {
  console.log(`Requesting user group for user with id ${id}`); // debug only

  const typeAdmin = { group: 'Administrator', rights: 'all' };
  const typeUser = { group: 'User', rights: 'restricted' };
  const timeout = Math.floor(Math.random() * 5000);

  return new Promise((resolve, reject) => {
    // Simulated API call with timeout
    setTimeout(() => {
      resolve(id === 0 ? typeAdmin : typeUser);
    }, timeout);
  });
}

const getUserDetails = function(id) {
  console.log(`Requesting user details for user with id ${id}`); // debug only

  const timeout = Math.floor(Math.random() * 5000);

  return new Promise((resolve, reject) => {
    // Simulated API call with timeout
    setTimeout(() => {
      resolve({ fullName: id === 0 ? 'Administator himself' : 'Some User' });
    }, timeout);
  });
}

Rx.Observable.of(initialUserObj)
  .flatMap(user => {
    return Rx.Observable.fromPromise(getUserGroup(user.userId))
      .map(group => Object.assign({}, user, group));
  })
  .flatMap(user => {
    return Rx.Observable.fromPromise(getUserDetails(user.userId))
      .map(details => Object.assign({}, user, details));
  })
  .subscribe(x => console.log(x));

Working sample on jsbin

Upvotes: 2

Related Questions