Give BehaviorSubject initial value - value being an Observable

12,860

I would do it like

private user = new BehaviorSubject<User>(null);
public user$ = this.user.asObservable();

constructor() {
  this.getUser();
}

getUser(): User {
  // does obviously not work!
  return this.apiService.getUser()
    .subscribe(data => {
      if(!!data) {
        this.user.next(data); // <<== added
      }
      // do something else
    })  
}
Share:
12,860
DRNR
Author by

DRNR

Updated on June 05, 2022

Comments

  • DRNR
    DRNR almost 2 years

    I know that I cannot give BehaviorSubject an Observable value, but I need a way to solve this issue. On app initialization I am fetching current user (if exists), and I need to give the BehaviorSubject that potential value. So my service code looks like this:

    private user = new BehaviorSubject<User>(this.getUser());
    public user$ = this.user.asObservable();
    
    getUser(): User {
      // does obviously not work!
      return this.apiService.getUser()
        .map(data => {
          if(data) {
            return data;
          }
          // do something else
        })  
    }
    

    So is there some magical rxjs operator to solve this issue, or some other possibility?

    Thanks in advance!

    • Jota.Toledo
      Jota.Toledo over 6 years
      what is the return type of the getUser method in the api service?
    • Jota.Toledo
      Jota.Toledo over 6 years
      Assuming that getUser will return an Observable<User>, you are AFAIK obligated to subscribe somewhere in your code (most likely constructor method) to give that BehaviorSubject an "initial value" (the value wont be available at the instantiation moment, will be passed by "next" call). So basically that getUser(): User method is impossible to implement.
    • olivarra1
      olivarra1 over 6 years
      @Jota.Toledo Not quite obligated: Can be achieved by concatenating observables. Take a look at my answer
  • Günter Zöchbauer
    Günter Zöchbauer over 6 years
    @Jota.Toledo thanks for the update, but I think do() also won't cause the observable to do anything. I changed it to subscribe().
  • Jota.Toledo
    Jota.Toledo over 6 years
    yup, just wanted to point that out. I guess the getUser returns an observable, so in this case you need to modify your answer still: you are returning a subscription,not an User instance
  • Jota.Toledo
    Jota.Toledo over 6 years
    Ill take a look later. Could you create a plnkr or fiddle to test this?
  • bryan60
    bryan60 over 6 years
    Could also add .filter(v => !!v) if you don’t want the initial null value to go through, depends on the case though.
  • bryan60
    bryan60 over 6 years
    This won’t work, it will run the api call on every subscription, which isn’t the goal. You’d need a second subject and a store pattern to make this work the way you intend
  • Jota.Toledo
    Jota.Toledo over 6 years
    Where do you push values to the subject?
  • olivarra1
    olivarra1 over 6 years
    I'm not pushing, OP does need that to be a subject, I guess that's because he wants to push. Else why bother creating a subject when you can instead just const user$ = this.getUser()?
  • olivarra1
    olivarra1 over 6 years
    OP Says "I need to initialize a Subject with something that comes from an observable". This is the way to do it. @Jota.Toledo plunkr: next.plnkr.co/edit/chVpEzUfm5uRZ13j
  • olivarra1
    olivarra1 over 6 years
    @bryan60 could use a ReplaySubject instead of a BehaviourSubject for that matter....
  • olivarra1
    olivarra1 over 6 years
    @bryan60 That depends on use case, but you can always .publish() the observable so it gets shared between subscribers
  • Jota.Toledo
    Jota.Toledo over 6 years
    the problem that I see with this is that the first observer that starts to get values from "user$" wont get notified when new values are pushed into "user", as it will be taking values from this.getUser() reference
  • bryan60
    bryan60 over 6 years
    Not true, the nature of switchMap, it switches when new values come through and cancels previous inner observables. One cool thing about this pattern is that you can cause server refreshes by passing null values through the source, due to the switching.
  • bryan60
    bryan60 over 6 years
    @Jota.Toledo FYI, your edit created an infinite loop, the way I had it is correct.
  • bryan60
    bryan60 over 6 years
    Actually, no not an infinite loop, just an odd double subscription, but I believe the way I have it is semantically cleaner.
  • olivarra1
    olivarra1 over 6 years
    I really don't think this is semantically cleaner... You have at least 2 side effects in a single line. Consider this, for instance, on the first loop, u = null, will get through this.getUser(). But this getUser() after the do() will yield one user that will be yielded to user$ because of switchMap. But inside do() you also call .next(nu), which will cause user$ to yield an extra user for the same one. If it's not like that, then I don't consider this semantically correct either - it's really complex to understand what's going on in there.
  • bryan60
    bryan60 over 6 years
    it's one side effect done intentionally, the way this actually plays outin practice is that the subscriber never gets the result of the getUser() call, because the next operation immediately cancels that subscription in the same thread and just reverts to the value that got passed through the source in the do operation, so the subscriber will only actually see one event as intended. It's a little complicated, but I actually think it's clear, subsscribe to the source, if it doesn't have a value, switch to this remote fetch method and save the value.
  • bryan60
    bryan60 over 6 years
    it's essentially the classic cache get and set scheme in observable format.
  • olivarra1
    olivarra1 over 6 years
    @bryan60 Well, in plunkr doesn't seem to work next.plnkr.co/edit/7ynJ7opLCQjqB3Gk (has the exact behaviour I described: duplicated initial value). With all respects, if you're not too sure why does this doesn't work, I wouldn't call this solution "semantically correct". It's far from it.
  • olivarra1
    olivarra1 over 6 years
    And another issue, what if you call .next(null) on purpose to set the user to null? This would refetch the user (and again, yield it twice in the result stream)
  • olivarra1
    olivarra1 over 6 years
  • bryan60
    bryan60 over 6 years
    Good point, this is still recoverable though, and it isn't necesarily bad if events trigger twice, depends on the case. and doens't have the drawback of repeating the API call on subsequent subscriptions, see my update.
  • bryan60
    bryan60 over 6 years
    causing the user to refresh by setting it to null is a feature, not a bug
  • Günter Zöchbauer
    Günter Zöchbauer over 6 years
    The return value isn't used at all. The next() is the relevant part
  • DRNR
    DRNR over 6 years
    Thank you for your answer, I will get back to you ASAP! :)
  • DRNR
    DRNR over 6 years
    Thank you for your answer, I will get back to you ASAP! :)
  • DRNR
    DRNR over 6 years
    Thank you for your answer, I will get back to you ASAP! :)
  • tif
    tif almost 5 years
    @GünterZöchbauer I've seen examples where they call getUser in the component on ngOnInit method. What is a better approach? In the consturctor of the service or in ngOnInit of the component using it?
  • Günter Zöchbauer
    Günter Zöchbauer almost 5 years
    If it depends on a value passed to an input, then you need ngOnInit, otherwise I don't see any advantage/disadvantage. It might be easier to unit-test if you keep code out of the constructor. It's mostly personal preference.
  • Günter Zöchbauer
    Günter Zöchbauer almost 5 years
    If it depends on a value passed to an input, then you need ngOnInit, otherwise I don't see any advantage/disadvantage. It might be easier to unit-test if you keep code out of the constructor. It's mostly personal preference.