Reputation: 2085
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
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
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):
this.getUserFromApi()
which executes the body of that method. This evaluates to an observable because that method returns an observable..map()
on that observable, which produces another observable.this.user
, which produces the current value of this.user
.Observable.of()
on the value from step 3, which produces an observable.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
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