5

Looking for the same functionality as BehaviorSubject but without having to provide an initial value.

I often need to subscribe to a subject before my data gets loaded from whatever source I load it from. I also need the subscription to fire immediately after subscribing if and only if next() was previously called on the subject and I expect it to show me the last emitted value, as well as be able to get the current value synchronously at any point in time, similar to the value field of BehaviorSubject.

Here's what I tried:

  • Subject, but it doesn't emit the old value immediately after subscribing to it like BehaviorSubject and I can't retrieve the current value.
  • ReplaySubject with a buffer of size 1, but this also doesn't allow retrieval of the current value.
  • BehaviorSubject.create() instead of new BehaviorSubject<Whatever>(null) but this just doesn't do anything when calling next(). No value is emitted and the subscription code never fires.
  • BehaviorSubject with .pipe(skip(1)).subscribe but if I run this code after I called next() then I loose that old but valid value, only firing after next is called again.

The only solution I came up with is to extend the ReplaySubject class to add a private field for the current value, which gets set when next() is called, and a getter to retrieve it.

I always expect these values to be defined and valid, so I don't understand why I have to provide a default null/undefined value to BehaviourSubject for an object type that I specifically expect to not be undefined/null and explicitly defined them accordingly, and then write an if statement in every subscription to check every time a new value is emitted if it is valid.

I also don't understand why must it emit that null value and cause each subscription to fire for no good reason each time I subscribe to the subject before I load the data.

So is there any alternative I have missed?

Edit: So far, the answer to my question seems to be that no, there isn't an equivalent. As such, I'm providing my work around for anyone who's looking for the same behavior:

import { ReplaySubject } from 'rxjs';

export class SensibleSubject<T>
{
    private replaySubject: ReplaySubject<T>;
    private _canGetValue = false;
    private _value!: T;

    public get value()
    {
        if (!this._canGetValue)
            throw new Error("Attempted to retrieve value before it has been set!");

        return this._value;
    }

    constructor()
    {
        this.replaySubject = new ReplaySubject<T>(1);
    }

    public next(value: T)
    {
        this._canGetValue = true;
        this._value = value;
        this.replaySubject.next(value);
    }

    public asObservable()
    {
        return this.replaySubject.asObservable();
    }

    public canGetValue()
    {
        return this._canGetValue;
    }
}

Initially, I specified that I extended the ReplaySubject class but, as crowmagnumb points out in his answer to Behaviour subject initial value null?, overriding next on the ReplaySubject somehow breaks it. I instead decided to wrap the ReplaySubject and expose the desired methods and fields.

user1969903
  • 810
  • 13
  • 26

1 Answers1

0

How about

src = new BehaviorSubject<any>(null);
obs$ = src.asObservable.pipe(filter(value => !!value));

Since you say all the values emitted to the source will be valid and not undefined, the filter pipe will only filter the initial default null. This way you don't have to do an if check in each subscription and still get to keep the synchronous getValue() function.

ruth
  • 29,535
  • 4
  • 30
  • 57
  • Yeah, but if instead of type ```any``` I provide an actual type to the BS (what a lovely coincidence that the initials match that of another equally useful concept), I get ```Argument of type 'null' is not assignable to parameter of type'[whatever type I specified]'.``` If I leave type ```any```, I loose type enforcing and all that jazz. – user1969903 Feb 24 '21 at 08:56
  • 1
    Union types? `new BehaviorSubject(null);` – ruth Feb 24 '21 at 08:58
  • I guess, but if I do that, then typescript wants me to explicitly cast the emitted value to ```someType```: ```someBsObservable.subscribe(result => this.someValue = result as someType)``` Otherwise I get nagged with ```Type 'someType | null' is not assignable to type 'someType'``` – user1969903 Feb 24 '21 at 08:59
  • If you want a specific behavior from a multi-cast observable, you need to implement your own in that case. Both [`BehaviorSubject`](https://github.com/ReactiveX/rxjs/blob/master/src/internal/BehaviorSubject.ts) and [`ReplaySubject`](https://github.com/ReactiveX/rxjs/blob/master/src/internal/ReplaySubject.ts) are an extension of `Subject`. You could start from there. Using standard multi-cast observables however you need to cast it like you said. – ruth Feb 24 '21 at 09:05
  • The solution I mentioned does exactly what I need, without casting. I'm just curious if that's the way to go or if there's actually an alternative I've missed. – user1969903 Feb 24 '21 at 09:09
  • @user1969903: No you have not – ruth Feb 24 '21 at 09:09
  • Could you please clarify the last comment? It's not clear if no, I have no other alternative or no, that is not the way to go or no, I have not mentioned a valid solution. – user1969903 Feb 24 '21 at 09:12
  • I meant you have not missed any other options you've mentioned to the question "_Is there an equivalent to BehaviorSubject that doesn't require an initial value and is able to provide the current value?_" – ruth Feb 24 '21 at 10:28
  • Thanks for clarifying. I've edited in my remaining proverbial 2 cents. I guess you can change your answer to add "No, there is no equivalent..." before your suggestion and I'll accept it. If the Angular / RxJS teams decide to implement this, we can update the answer. – user1969903 Feb 25 '21 at 08:07