angularrxjsreactive-programmingrxjs-observablesrxjs-pipeable-operators

Toggling value of behavior subject without using getValue


I want to toggle the boolean value of my behavior subject. I'm currently achieving this by using the Behavior Subject's getValue() method. This isn't ideal as I'm effectively taking my reactive, declarative code and turning it into imperative style code by pulling the data out of the data stream:

private userIsRegistering: BehaviorSubject<boolean> = new BehaviorSubject(false);
userIsRegistering$: Observable<boolean> = this.userIsRegistering.asObservable();

toggleUserIsRegistering() {
   this.userIsRegistering.next(!this.userIsRegistering.getValue());
}

I want to do this in a reactive way by somehow making use of RXJS operators and working within the data stream. I can't seem to figure how to go about this?

I've seen this SO post:

RXJS - BehaviorSubject: proper use of .value

But the issue with this is that the myBool$ Observable only gets triggered when the myToggle$ Observable is triggered.

I need the userIsRegistering Subject/Behavior Subject to have an initial value as I am deriving state from this Observable in other parts of the service:

accountAction$: Observable<{type: string, linkLabel: string, linkText: string, buttonAction: string}> = this.userIsRegistering$.pipe(
    map(userIsRegistering => {
      return {
        type: userIsRegistering ? 'Register' : 'Login',
        linkLabel: userIsRegistering ? 'Already have an account?' : 'No account?',
        linkText: userIsRegistering ? 'Login' : 'Register',
        buttonAction: userIsRegistering ? 'Register' : 'Login',
      }
    })
  );

If the Behavior Subject did not have an initial value, the above accountAction$ Observable would not be triggered when the service initializes.

Anyone have any ideas?


Solution

  • But the issue with this is that the myBool$ Observable only gets triggered when the myToggle$ Observable is triggered.

    If you simply need to ensure derived observables emit an initial value, just use startWith:

      private myToggle$ = new Subject<void>();
    
      private myBool$ = this.myToggle$.pipe(
          scan(previous => !previous, true),
          startWith(true),
      );
    
      state$ = this.myBool$.pipe(
        map(bool => bool ? STATE_A : STATE_B)
      );
    

    Here, myToggle$ is simply a trigger (emitted value is not used). myBool$ uses scan to emit the new state based on the previous emission. startWith will initially emit the default value.


    If you need the multicast / replay functionality like BehaviorSubject provides, you could simply use shareReplay(1):

      private myBool$ = this.myToggle$.pipe(
          scan(previous => !previous, MY_BOOL_INITIAL),
          startWith(MY_BOOL_INITIAL),
          shareReplay(1)
      );
    

    Here's a little StackBlitz demo.