Home > OS >  How to initialize an observable backed by a BehaviorSubject on first call?
How to initialize an observable backed by a BehaviorSubject on first call?

Time:10-05

I have an Observable that is backed by a private BehaviorSubject. On the first emit, I would like to initialize the BehaviorSubject from an async call, but I can't come up with a great pattern to do so. As far as I can tell, BehaviorSubjects can't be initialized from an async function.

What I have so far is:

protected _monkeyNames = new BehaviorSubject<Set<string>>(null);

MonkeyNames$: Observable<Set<string>> = this._monkeyNames.pipe(
  switchMap(async (nodes) => nodes ?? (await this.getMonkeyNames()))
);

protected async getMonkeyNames(): Promise<Set<string>> {
  const names = new Set(await this.stateService.getMonkeyNames());
  return names;
}

But this won't set the BehaviorSubject, it will only be set when I call setMonkeyNames later to save a new value. If I do call .next() inside of getMonkeyNames, the Observable will emit again, which could result in an eternal loop if names is null.

It might be my own ignorance when it comes to rxjs, but does anyone have a pattern they use for this?

Edit: I should mention that this is a service, and I won't have access to ngOnInit()

CodePudding user response:

BehaviorSubject is useful only when there is a initial value. Since you don't have any, ReplaySubject or Subject can be used. Also initializing BehaviorSubject with null, when type clearly restricts to Set<...>, is just an error.

If your service returns promise of some value, convert it to Observable with from and process in operator chain.

monkeyNames$: Observable<Set<string>> = from(this.stateService.getMonkeyNames())
.pipe(
map(names=> new Set(names))
)

CodePudding user response:

Instead of attempting to initialize the subject, maybe you can declare your MonkeyNames$ from two different sources using merge:

protected _monkeyNames = new Subject<Set<string>>();

MonkeyNames$: Observable<Set<string>> = merge(
    from(this.getMonkeyNames()),
    this._monkeyNames
).pipe(
    shareReplay(1)
);

CodePudding user response:

What you can do is initialize the BehaviorSubject with an empty Set, then do the api call or async call in the constructor of the service that would set the initial value for the first time.

protected _monkeyNames = new BehaviorSubject<Set<string>>(new Set());

MonkeyNames$: Observable<Set<string>> = this._monkeyNames.asObservable();

protected async getMonkeyNames(): Promise<Set<string>> {
  const names = new Set(await this.stateService.getMonkeyNames());
  this._monkeyNames.next(names);
  return names;
}

constructor() {
  this.getMonkeyNames();
}

Example HTML Template:

<div *ngIf="MonkeyNames$ | async as names">
  <div *ngFor="let name of names">{{ name }}</div>
</div>

Here is a working Stackblitz example (with some extra debugging; see console).

  • Related