0

I'm wanting to implement an Observable / Subject with 3 particular attributes

  1. Remember last emitted value and be able to surface it via a getter (BehaviorSubject)
  2. Only emit when value changes
  3. It must have a strong type such that the getter is known to be available by a consumer (aka. BehaviorSubject.getValue())

I'm thinking of just extending BehaviorSubject but want to make sure I'm not introducing any potential gotchas based on my novice understanding.

export class DistinctUntilChangedBehaviorSubject<T, TValue> extends BehaviorSubject<T> {
    constructor(
        initialValue: T,
        private _distinctKeySelector?: (value: T) => TValue,
        private _comparer?: _Comparer<TValue, boolean>
    ) {
        super(initialValue);
    }

    public subscribe() {
        // I'm particularly interested in knowing if this has any gotchas. 
        // Mostly things like creating subscriptions that don't get disposed as expected.
        return super.distinctUntilChanged(
            this._distinctKeySelector,
            this._comparer
        ).subscribe.apply(this, arguments);
    }
}

So 2 questions:

  1. Does this seem like a reasonable approach / are there any gotchas here?
  2. Is there another preferred way of doing this?
bingles
  • 11,582
  • 10
  • 82
  • 93

2 Answers2

2

I do not know really why, but I tend to prefer composition over extension.

So I would do something along these lines

import {BehaviorSubject} from 'rxjs';

export class BehaviourSubjectAugmented<T> {
    bs: BehaviorSubject<T>;

    constructor(initialValue: T, private comparer: (p: T, q: T) => boolean) {
        this.bs = new BehaviorSubject(initialValue);
    }

    getValue() {
        return this.bs.getValue();
    }

    asObservable() {
        return this.bs.asObservable()
                        .distinctUntilChanged(this.comparer);
    }

    complete() {
        return this.bs.complete();
    }
    next(value: T) {
        return this.bs.next(value);
    }

}
Picci
  • 16,775
  • 13
  • 70
  • 113
  • I tend to prefer composition as well just didn't want to have to wrap too many things. In your example, does asObservable() get called when you subscribe to the BehaviourSubjectAugmented object, or would you have to subscribe to the result of asObservable() ? – bingles Mar 22 '18 at 11:36
  • As it is coded now you need to do `behaviourSubjectAugmented.asObservable().subscribe()`. – Picci Mar 22 '18 at 12:19
  • Hmm, guessing this also means that chained operators wouldn't be present unless you call asObservable. – bingles Mar 22 '18 at 12:44
  • `asObservable()` makes sure that you can not use the `next` method to emit values (see https://stackoverflow.com/questions/36986548/when-to-use-asobservable-in-rxjs or https://stackoverflow.com/questions/42272821/observable-vs-asobservable). If this is not important for you, then you can use directly `bs` property – Picci Mar 22 '18 at 13:07
1

Turns out my original idea causes a call stack exceeded issue. I'm assuming that distinctUntilChanged must call subscribe internally thus causing infinite recursion.

I ended up finding a simpler way to get what I needed by simply adding a method to an ISubject instance.

function distinctUntilChangedBehaviorSubject(
    initialValue: number
): ISubject<number> & { getValue(): number } {
    const observer = new BehaviorSubject<number>(initialValue);
    const observable = observer.distinctUntilChanged();

    const subject: ISubject<number> = Subject.create(
        observer,
        observable
    );

    return Object.assign(
        subject,
        {
            getValue: () => observer.getValue()
        }
    );
}
bingles
  • 11,582
  • 10
  • 82
  • 93