Slava Fomin II
Slava Fomin II

Reputation: 28611

Make observable to behave like promise in RxJS?

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:

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?


Update

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

Answers (2)

martin
martin

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

CozyAzure
CozyAzure

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

Related Questions