How to toggle rxjs variable?

Viewed 352

How correct is it to do a toggle variable?

public toggleLabels$ = new BehaviorSubject<boolean>(true);

public toggleLabels() {
    this.toggleLabels$.next(!this.toggleLabels$.getValue());
}

I just get reverse value and push it back. Is it good way?

3 Answers

With scan RxJS has a built in state managing solution that can be used for all state scenarios (also some that are more complex than a toggle mechanism).

Useful for encapsulating and managing state. Applies an accumulator (or "reducer function") to each value from the source after an initial state is established -- either via a seed value (second argument), or from the first value from the source.

const { Subject } = rxjs;
const { scan, startWith } = rxjs.operators;

const toggle$$ = new Subject();

const toggle$ = toggle$$.pipe(
  scan((state, curr) => !state, true),
  startWith(true)
);

toggle$.subscribe(console.log)

toggle$$.next()
toggle$$.next()
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/7.4.0/rxjs.umd.min.js"></script>

Bryan60 mentioned that my answer does not implement the behavior exactly as the question logic is. There are two conceptual differences:

  • new subscriber wont get the last value
  • no subscriber means the observable wont calculate/emit its pipe For both missing features there is a specific solution if needed:

New subscriber (replay last value): The shareReplay operator allows you to share an observable and replay the last emitted value

Share source and replay specified number of emissions on subscription.

No subscriber (pipe wont emit): connectable creates you an observable wich provides a connect method. This can be called to make your observable hot and subscribed. Then it will emit values without having a subscription.

Info: Both features (replaying and connecting) is most of the time not needed. In my last project we saw startup issues with an overuse of BehaviorSubjects and multicasting. If only one subscription happens a time and the subscription is done before values are emitted no replaying mechanism is needed. Please keep always in mind to only conscious implement features as the may have impact on startup-, runtime-performance or give your application an unexpected and incosistent behavior.

const { Subject, connectable } = rxjs;
const { scan, startWith, shareReplay } = rxjs.operators;

const toggle$$ = new Subject();

const toggle$ = toggle$$.pipe(
  scan((state, curr) => !state, true),
  startWith(true),
  shareReplay(1)
);

connectable(toggle$).connect()

toggle$.subscribe(v => console.log("first subscribe: ", v))

toggle$$.next()
toggle$$.next()

toggle$.subscribe(v => console.log("second subscribe: ", v))
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/7.4.0/rxjs.umd.min.js"></script>

this is fine for something this simple but getValue() isn't something I'd make a habit of using. it can get messy and break in more complex situations.

you generally want to keep state modifications inside the observable to make sure everything happens in the order it's supposed to.

here's how you could do that:

// two subjects, one for saving state, one for signaling toggles
private toggleSource = new Subject();
private toggleLabelsSource = new BehaviorSubject<boolean>(true);
// keep state observables private
toggleLabels$ = this.toggleLabelsSource.asObservable();

constructor() { 
  // link observables
  this.toggleSource.pipe(
    scan((toggle, _) => !toggle, true)
  ).subscribe(this.toggleLabels$);
}

public toggleLabels() {
  // call next on toggle signal.
  this.toggleSource.next(null);
}

this is overly complex for something so simple, but the pattern makes more sense when you're dealing with more complicated state transformations.

Create a service that contains:

  private toggleValue = true;
  private toggleLabelsSource = new BehaviorSubject<boolean>(this.toggleValue);
  currentToggleLabels = this.toggleLabelsSource.asObservable();


  toggleLabels(): void {
    this.toggleValue = !this.toggleValue;
    this.toggleLabelsSource.next(this.toggleValue);
  }

Then you can listen to currentToggleLabels and toggle through toggleLabels()

Related