Reputation: 28611
Considering I have a function which calls a server and returns user data through Observable:
getUserById (id: number): Observable<User> {
return this.http.get('/users/' + id)
.map(response => response.json())
;
}
How do I make it to behave more like a Promise?
The problem is, that the request won't be made until someone subscribes to the returned observable. Also, when multiple subscriptions are done, the server will be called multiple times.
What I want is:
To call server as soon as getUserById()
is called (the subscription should be optional, only if caller really wants to obtain the returned value or cares to know when request is complete)
Multiple subscriptions should not spawn multiple requests, but should return the same value obtained from the server
With this approach I will be able to make this function work:
refreshUser (): Observable<User> {
return repository.getUserById(this.currentUser.id)
.map(user => this.currentUser = user)
;
}
When calling refreshUser()
the request should be made and this.currentUser = user
should be evaluated. If I want, I can subscribe to the returned observable in order to obtain fresh data, e.g:
// Just refreshes the current user
refreshUser();
// Refreshes the current user and obtains fresh data
refreshUser().subscribe(user => console.log('Refreshed user', user));
I've tried to use publish()
, refCount()
and share()
in different combination, but it only helps with the first requirement (i.e. avoiding multiple calls to the server), but how do I make the original observable hot?
After further investigation with different options it seems like solution with making getUserById()
hot will not solve it, because I have two mapping functions on different levels (one in getUserById()
and one in refreshUser()
) and I need both of them to execute. In order to solve this, I need to somehow "push" the observable from bottom to top. And it looks like it will require a lot of boilerplate code for each level.
In promises it will look pretty simple:
getUserById (id: number): Promise<User> {
return this.http.get('/users/' + id)
.map(response => response.json())
.toPromise()
;
}
refreshUser (): Promise<User> {
return repository.getUserById(this.currentUser.id)
.then(user => {
this.currentUser = user;
return user;
})
;
}
Is there a simpler solution with RxJS or Promises are actually better suited for the job?
Upvotes: 7
Views: 1676
Reputation: 96891
From your description it looks like you could do something like this:
getUserById (id: number): Observable<User> {
const observable = this.http.get('/users/' + id)
.map(response => response.json())
.publishReplay(1)
.refCount()
.take(1);
observable.subscribe();
return observable;
}
If you subscribe to the same Observable returned from getUserById
multiple times you'll always get the same result.
Upvotes: 4
Reputation: 8468
Looks like you will need to use a or Subject
or a BehaviorSubject
instead of just a normal Oberservable. In your case, a BehaviorSubject
is better as it stores the current value.
Declare two variables, one being a normal User
type, and the other one is a BehaviorSubject
of User
:
currentUser:User;
userBSubject:BehaviorSubject<User>;
Because you will need an initial value for the BehaviorSubject
, you can initialize it at your constructor:
constructor(){
this.userBSubject = new BehaviorSubject<User>(this.currentUser)
}
So here is where the magic comes in. Every time you need to modify (in your case, "refresh") the data, you will call the method .next()
:
refreshUser() {
return repository.getUserById(this.currentUser.id)
.subscribe(user => {
this.userBSubject.next(user);
return this.userBSubject.asObservables()
});
}
Then you can do:
// Just refreshes the current user
refreshUser();
// Refreshes the current user and obtains fresh data
refreshUser().subscribe(user => console.log('Refreshed user', user));
Upvotes: 1