How do I force a refresh on an Observable service in Angular2?
Asked Answered
M

2

36

In ngOnInit, my component obtains a list of users like so:

this.userService.getUsers().subscribe(users => {
    this.users = users;
});

And the implementation of userService.getUsers() looks like this:

getUsers() : Observable<UserModel[]> {
    return this.http.get('http://localhost:3000/api/user')
                    .map((res: Response) => <UserModel[]>res.json().result)
                    .catch((error: any) => Observable.throw(error.json().error || 'Internal error occurred'));
}

Now, in another component, I have a form that can create a new user. The problem is that when I use that second component to create a user, the first component doesn't know that it should make a new GET request to the backend to refresh its view of users. How can I tell it to do so?

I know that ideally I'd want to skip that extra HTTP GET request, and simply append the data the client already has from when it made the POST to insert the data, but I'm wondering how it'd be done in the case where that's not possible for whatever reason.

Microscopy answered 25/10, 2016 at 21:1 Comment(0)
S
21

In order for an observable to be able to provide values after initial Http observable was completed, it can be provided by RxJS subject. Since caching behaviour is desirable, ReplaySubject fits the case.

It should be something like

class UserService {
  private usersSubject: Subject;
  private usersRequest: Observable;
  private usersSubscription: Subscription;

  constructor(private http: Http) {
    this.usersSubject = new ReplaySubject(1);
  }

  getUsers(refresh: boolean = false) {
    if (refresh || !this.usersRequest) {
      this.usersRequest = this.http.get(...).map(res => res.json().result);

      this.usersRequest.subscribe(
        result => this.usersSubject.next(result),
        err => this.usersSubject.error(err)
      );
    }

    return this.usersSubject.asObservable();
  }
  onDestroy() {
    this.usersSubscription.unsubscribe();
  }
}

Since the subject already exists, a new user can be pushed without updating the list from server:

this.getUsers().take(1).subscribe(users => 
  this.usersSubject.next([...users, newUser])
)
Slender answered 25/10, 2016 at 22:46 Comment(13)
Lots of great answers here. I went with the approach you suggested, and it's working great.Microscopy
how to unsubscribe using this method ?Asturias
The same way as it would be done with any other observable; use this.usersSubject.unsubscribe() or save a subscription from observable and call unsubscribe on it in service ngOnDestroy. Since Http results in completed observables, there may be no need to unsubscribe, though it would be better to do that.Slender
by tring the solution i got this error Generic type 'Subject<T>' requires 1 type argument(s).Applicant
@IlyesGHOMRANI This is likely specific to later RxJS version. If this is the case, provide type argument like the error suggests, e.g. foo: Subject<FooType>.Slender
It's not good practise to subscribe to observable in services. I would instead use pipe and tap: this.usersRequest = this.http.get(...).pipe(map(...), tap((result) => this.usersSubject.next(result), switchMap(() => this.usersSubject.asObservable()));Japeth
@Japeth I'd not say it's a bad practice, but it's a good practice to unsubscribe. See my comment above on subscriptions. The code you suggested looks fine but from my understanding of how you suggest it to use, the intention differs. Since an observable can be resubscribed by consumers, the service will do unnecessary requests again, while still returning cached data via usersSubject. If you have ideas how it can cache without making requests when no refresh is provided, consider posting an answer with derived code.Slender
@estus my snippet was just to give an idea (as it's not a complete snippet) of replacing the block within if (refresh || !this.usersRequest), not to replace the whole method, otherwise, of course the caching is lost. If I get some spare time I will post a full answer. Ok maybe it's not a documented bad practise ;) but from my experience and readings it makes for cleaner code when .subscribe() are left out of services. Once you get use to using operators like tap, switchMap, concatMap, and flatMap, you never need to anyway. Just my advice :)Japeth
@Japeth I understand which kind of subscribe cases you mean. Yes, it would be better to start a request with getUsers().subscribe(...). I don't remember the concerns I had in mind when I wrote this, but I'm positive that the same caching behaviour cannot be expressed only with operators you listed. subscribe is perfectly ok for low-level things, it's widely used in RxJS internals. I suppose this could be achieved in more elegant way with multicast and connect but didn't test that.Slender
an Observable can use async pipes ( i.e. ... | async in template files) which eliminates the boilerplate of the subscribe and ubsubscribe bookkeeping. - Which leads me to the question, can a ReplaySubject use async pipes ?Buffybuford
@Buffybuford A subject is observable so yes, it can be unsubscribed automatically by a pipe if a subscription is done on it. In current example there are multiple subjects to make this work as intended, notice that it's usersRequest that is unsubscribed manually, not usersSubject (replay subject). It needs to be unsubscribed once on service destroy.Slender
@estus I have a form that can edit as well as create a new user. How would I 'patch' the edited user into the list?Sheathbill
@Sheathbill As for the code that was posted in the answer, this.usersSubject.next([...users, newUser]) is replaced with ``this.usersSubject.next(users), where users` has been previously modified according to your needs. I cannot say how this should be done in your case because it depends on details of your implementation.Slender
N
3

The way I've handled this problem so far has been to use an intermediary. this.userService.getUsers() returns an Rx.BehviorSubject which is initialized on the return of the http observable.

Something close to this:

getUsers() : BehaviorSubject<UserModel[]> {
  return this.behaviorSubject;
}
updateUsers() {
    this.http.get('http://localhost:3000/api/user')
                    .map((res: Response) => <UserModel[]>res.json().result)
                    .catch((error: any) => Observable.throw(error.json().error || 'Internal error occurred'))
                    .subscribe((value) => {
                      this.behaviorSubject.next(value)});
}
Nightshirt answered 25/10, 2016 at 21:26 Comment(3)
This is technically the simplest possible 'redux' store :-) Same concept.Superordinate
This is a bit out of scope for this question perhaps, but ... Does a behaviorSubject work with an async pipe ?Buffybuford
@Buffybuford woah, a dead comment resurrection. Behavior Subject should work with an async pipe. Been quite a while since I've worked in Angular but based on this comment chain on an issue with Rx Subject, behavior subjects work fine with async pipe. github.com/angular/angular/issues/12129Nightshirt

© 2022 - 2024 — McMap. All rights reserved.