21

I have a parent observable that, once it has a subscriber, will do a lookup and emit a single value, then complete.

I'd like to convert that into an observable (or behavior subject or whatever works) that does the following: once it has at least one subscriber, it gets the result from the parent observable (once). Then it emits that value to all of its subscribers, and also emits that single value to all future subscribers, when they subscribe. It should continue with this behavior even if its subscriber count drops to zero.

It seems like this should be easy. Here is what didn't work:

theValue$: Observable<boolean> = parent$
.take(1)
.share()

Other things that didn't work: publishReplay(), publish(). Something that worked better:

theValue$ = new BehaviorSubject<boolean>(false);

parent$
.take(1)
.subscribe( value => theValue$.next(value));

There is a problem with this approach, though: parent$ is subscribed to before theValue$ gets its first subscriber.

Is there a better way to handle this?

Karptonite
  • 1,490
  • 2
  • 14
  • 30

3 Answers3

17

shareReplay should do what you want:

import 'rxjs/add/operator/shareReplay';
...
theValue$: Observable<boolean> = parent$.shareReplay(1);

shareReplay was added in RxJS version 5.4.0. It returns a reference counted observable that will subscribe to the source - parent$ - upon the first subscription being made. And subscriptions that are made after the source completes will receive replayed notifications.

shareReplay - and refCount in general - is explained in more detail in an article I wrote recently: RxJS: How to Use refCount.

cartant
  • 57,105
  • 17
  • 163
  • 197
  • This seems to work! I'm going to play with it for a bit, to make sure that there are no surprises. This looks great--I only wish they had added this to the docs when they created it. I don't like to feel like I'm using an undocumented feature that could go away. But I see it as added in the changelog, so I'm guessing it is here to stay. – Karptonite Sep 13 '17 at 22:46
  • It's there to stay. The docs are a work in progress. Anyway, it's very close to `publishReplay(1).refCount()` - the difference is subtle. – cartant Sep 14 '17 at 01:02
  • After reading your very thorough blog posts, I think that what I may actually want here is `publishLast().refCount`, although in practice, they may be functionally nearly identical in my case. Thanks again! – Karptonite Sep 14 '17 at 15:32
  • Unfortunately this doesn't allow using BehaviorSubject.value property because the result of this operation is still just an Observable. – Celestis Feb 18 '19 at 07:02
  • 2
    @Celestis Using a subject's `value` property is generally considered to be a code smell. I certainly consider it to be bad practice. – cartant Feb 18 '19 at 09:38
  • @Celestis If you need the last emitted value, maybe the `scan` operator could be useful. Don't use a subject's `value` property. – Patrick May 24 '22 at 08:54
10

I've implemented a method to convert Observables to BehaviorSubjects, as I think that the shareReplay method isn't very readable for future reference.

import { Observable } from 'rxjs/Observable';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';

export function convertObservableToBehaviorSubject<T>(observable: Observable<T>, initValue: T): BehaviorSubject<T> {
    const subject = new BehaviorSubject(initValue);

    observable.subscribe(
        (x: T) => {
            subject.next(x);
        },
        (err: any) => {
            subject.error(err);
        },
        () => {
            subject.complete();
        },
    );

    return subject;
}
tmuecksch
  • 6,222
  • 6
  • 40
  • 61
  • 1
    Is there still no built in way to do this yet? – chrismarx May 24 '19 at 16:00
  • 1
    @chrismarx nope. as far as I know, the devs don't have any intentions to add this feature – tmuecksch Aug 28 '19 at 09:27
  • 1
    This is so simple yet brilliant! The only requirement for a `BehaviorSubject` is indeed a initial value which can be, for example, an empty array if the observable is fetching from a service! Thank you! – CPHPython Oct 10 '19 at 14:57
  • 2
    Use this solution with caution because the derived subject never unsubscribes from the original observable, it can cause a memory leak – Finesse Nov 10 '19 at 11:16
4

This is an improved variant of the tmuechsch's answer.

import { Observable, BehaviorSubject } from 'rxjs';

export function convertObservableToBehaviorSubject<T>(observable: Observable<T>, initValue: T): BehaviorSubject<T> {
  const subject = new BehaviorSubject(initValue);
  const subscription = observable.subscribe(subject);
  return {
    subject,
    stopWatching: () => subscription.unsubscribe()
  };
}

Be careful because the returned subject never unsubscribes from the source observable. You need to call stopWatching manually when you know that there are no more references to subject (e.g. when a view component is destroyed/unmounted). Otherwise you'll get a memory leak.

It's impossible to make an absolutely safe solution for the given problem. The reason is that a behavior subject has a value attribute that must always be updated even if the subject isn't subscribed to, therefore you can't unsubscribe from observable automatically when everybody unsubscribes from subject.

The cartant's solution isn't perfect too, because the result is not instanceof BehaviorSubject and shareReplay records the values only when it's subscribed to.

Finesse
  • 9,793
  • 7
  • 62
  • 92