Arikael
Arikael

Reputation: 2085

rxjs Observable.if always executes both statements when using map

I'm trying to decide which observable to return but the method getUserFromApi is always executed. The idea is that no local user is present the service gets the user from the api otherwise it should just return the existant user.

export class  UserTestService {

 private user: User;

 getUserFromApi(): Observable<User> {
    const user = new User('John', 'Do', 'test')
    return Observable.of(user);
  }

  getCurrentUser(): Observable<User> {
    return Observable.if(() => this.user == null, this.getUserFromApi().map((user: User) => {
        this.user = user;

        return this.user;
      }),
      Observable.of(this.user));
  }

  deleteCurrentUser(): void {
   this.user = new User('', '', '');
  }
}

And here's how I execute it (in a jasmine test)

userTestService.deleteCurrentUser();
userTestService.getCurrentUser().subscribe((user: User) => {
      expect(user.username).toEqual('');
      done();
    });

I assume that it is because this.getUserFromApi in Observable.if gets executed too early. But why is that and how can I solve it?

Upvotes: 3

Views: 493

Answers (3)

Richard Matsen
Richard Matsen

Reputation: 23483

@JLRishe is correct, the 2nd parameter to Observable.if(..., this.getUserFromApi()..., ... is invoked immediately in order to pass it in. (Took me ages to understand that. The observable mind-set!)

Wrapping the 2nd param with Observable.defer works, but you can also change the Observable.if for a ternary expression

getCurrentUser(): Observable<any> {
  // return Observable.if(
  //   () => this.user == null,
  //   this.getUserFromApi().map((user: User) => {
  //     this.user = user;
  //     return this.user;
  //   }),
  //   Observable.of(this.user)
  // );
  return this.user == null
    ? this.getUserFromApi().map((user: User) => {
        this.user = user;
        return this.user;
      })
    : Observable.of(this.user)

}

Here is a working StackBlitz.

Note, the console.log() inside getUserFromApi() never gets called when the following sequence is excuted, because deleteCurrentUser() sets user to a default object.

userTestService.deleteCurrentUser();
userTestService.getCurrentUser().subscribe((user: User) => {
  expect(user.username).toEqual('');
  done();
  console.log('testing', user)
});

Upvotes: 1

JLRishe
JLRishe

Reputation: 101720

The reason this is happening is that by JavaScript execution rules, this.getUserFromApi() is called immediately (synchronously), so there is no way that Observable.if can exert any control over whether it is called or not:

return Observable.if(
    () => this.user == null, 
    this.getUserFromApi().map((user: User) => {
        this.user = user;

        return this.user;
    }),
    Observable.of(this.user));

The series of actions that take place when executing this statement are (in this order):

  1. Evaluate this.getUserFromApi() which executes the body of that method. This evaluates to an observable because that method returns an observable.
  2. Call .map() on that observable, which produces another observable.
  3. Evaluate this.user, which produces the current value of this.user.
  4. Evaluate Observable.of() on the value from step 3, which produces an observable.
  5. Call Observable.if(), passing it the lambda function () => this.user == null, the observable from step 2, and the observable from step 4.

As we can see here, calling this.getUserFromApi() is the first thing that takes place, regardless of what () => this.user == null eventually evaluates to.

I believe that cartant's answer provides a way to ensure that this.getUserFromApi() is only called when the function in the first argument produces a true value, so I would recommend trying that out if you do indeed want to only call this.getUserFromApi() under that circumstance (which seems like a good idea).

Upvotes: 3

cartant
cartant

Reputation: 58410

getUserFromApi is always going to execute, as it has to be called to obtain the observable that's passed as the second argument to Observable.if.

If you do not want getUserFromApi to execute until after the if expression is evaluated, you can use defer:

getCurrentUser(): Observable<User> {
  return Observable.if(
    () => this.user == null,
    Observable.defer(() => this.getUserFromApi().map((user: User) => {
      this.user = user;
      return this.user;
    })),
    Observable.of(this.user)
  );
}

Upvotes: 3

Related Questions