Reduce if/else using RxJS

Make your code more readable & testable by reducing if/else

·

5 min read

Reduce if/else using RxJS

Hello friends 👋

Parham here, this time with a quick tip about how I use RxJS to reduce if/else.

There are already many blogs written on why if/else might be bad or how to reduce if/else statements in your code.

You might have seen people asking questions like, "Why is the 'if' statement considered evil?", "Why is it a bad programming practice to use if/else?" or similar ones.

Here is a popular blog example on techniques on how to reduce if/else javascript.plainenglish.io/6-tips-to-improv..

In my opinion, there is nothing wrong with if statements but overuse of if/else and especially nested if/else statements can add more complexity to the code, make it less readable and more error prone.

Some functional libraries like ramdajs even have functions like, ifElse, unless, when and cond to solve this problem by using a function instead of if/else blocks.

ramdajs.com/docs/#ifElse

const incCount = R.ifElse(
  R.has('count'),
  R.over(R.lensProp('count'), R.inc),
  R.assoc('count', 1)
);
incCount({});           //=> { count: 1 }
incCount({ count: 1 }); //=> { count: 2 }

So overall it's better if you can write your code in a way that avoids branching off and needs fewer if/else statements. Writing functional code can be one more trick in your toolbelt that will eliminate the need for nested if/else in the first place.

Here is my take on utilising RxJS for the same purpose.

The code example

For a bit of context of how the code example is structured, here is what I am trying to achieve.

I am trying to decide where the user should land when they open the app.

The logic in plain English would be something like this:

  • If this is the first time users visit the app, show Terms & conditions to agree to T&C. App will save and remember this choice. It's a one-off thing.

  • Then If the user has agreed to T&C and has not viewed the app intro, show the introduction. This gives the user a tour of the app features and only happen once as well. So the app keeps a flag remembering this as well.

  • If the user has agreed to T&C and has viewed the intro, take them to the home page.

The code uses TypeScript and Angular, but no major dependency on these tech stacks, and you can replicate in any other tech stacks like React or Vue easily.

Original code: I came across this code in one of the code reviews.

public enter(): Subscription {
    return this.termsConditionsStateSelector.agreed()
      .pipe(
        switchMap((termsAgreed) => this.helpStateSelector.viewed()
          .pipe(
            map((helpViewed) => ({helpViewed, termsAgreed})),
          ),
        ),
        tap(({ termsAgreed, helpViewed }) => {
          if (!termsAgreed) {
            this.navCtrl.navigateRoot('/terms-conditions');
          } else if (termsAgreed && !helpViewed) {
            this.navCtrl.navigateRoot('/help');
          } else {
            this.navCtrl.navigateRoot('/home');
          }
        }),
      ).subscribe();
  }

Refactor Option 1: Use withLatestFrom instead of that weird switchMap.

  public enter(): Subscription {
    return this.termsConditionsStateSelector.agreed()
      .pipe(
        withLatestFrom(this.helpStateSelector.viewed()),
        tap(([termsAgreed, helpViewed]) => {
          if (!termsAgreed) {
            this.navCtrl.navigateRoot('/terms-conditions');
          } else if (termsAgreed && !helpViewed) {
            this.navCtrl.navigateRoot('/help');
          } else {
            this.navCtrl.navigateRoot('/home');
          }
        }),
      ).subscribe();
  }

Refactor option 2: Completely remove the if/else.

 private showTnC(): void {
    const showTnC$ = this.termsConditionsStateSelector.agreed()
      .pipe(
        filter((agree) => !agree),
        take(1),
        tap(() => this.navCtrl.navigateRoot('/terms-conditions')),
      );
    showTnC$.subscribe();
  }
 private showIntro(): void {
    const showIntro$ = this.termsConditionsStateSelector.agreed()
      .pipe(
        filter((agree) => agree),
        withLatestFrom(this.helpStateSelector.viewed()),
        filter(([, viewed]) => !viewed),
        take(1),
        tap(() => this.navCtrl.navigateRoot('/help')),
      );
    showIntro$.subscribe();
  }
 private showHome(): void {
    const showHome$ = this.helpStateSelector.viewed()
      .pipe(
        filter((viewed) => viewed),
        take(1),
        tap(() => this.navCtrl.navigateRoot('/home')),
      );
    showHome$.subscribe();
  }
 public enter(): void {
    this.showTnC();
    this.showIntro();
    this.showHome();
  }

What do you think about the readability of code in these two examples?

Option 1 is easier from understanding the control flow, but Option 2 is more modular and functional.

I like Option 2 better because:

  • There is no if/else. ✅
  • Each function is doing a single job, so concerns are separated.
  • Code is more modular and therefore easier to test.
  • Functions are named based on my DSL.
  • Code is more functional, and there is less side effect.
  • I know how many signals to expect to use the take() operator and do not need to worry about unsubscribing?!! (Not exactly correct always, depending on the code flow)

⛔️ A note on unsubscribing and memory leak issues based on your code flow

Using filter() with take(1) can lead to a memory leak. For example, imagine agreed === true at the time of subscription, then showTnC$ will never get a signal and therefore never completes the subscription, even after the component is destroyed. In other words, it will be waiting forever. (potential memory leak). If this is a component that you can reinitialise multiple times (like leaving a view and come back to it), you will have multiple instances of these subscriptions, leading to your app misbehaving.

You have three solutions to fix this problem.

  1. Use a destroy signal and the takeUntil(this.destroyed$) pattern with destroyed$ emitting a signal as soon a subscription is not required. For example, if I get a signal to showIntro$, Probably I can send a destroy signal for my showTnC$ and use the takeUntil operator, assuming there is no further use for showTnC$.
  2. Skip the subscription altogether, use the async pipe in the template, and let Angular take care of unsubscribing.
  3. Keep a reference to your subscription and unsubscribe when the component is destroyed (ngOnDestroy). If you want to do this, I suggest adding an array of subscriptions to your class and push every new subscription to this array. Later at the component destroy time, you can loop over your subscriptions array and unsubscribe from each.

Something like this:

public subscriptions: Subscription[] = [];

 private showHome(): void {
    const showHome$ = this.helpStateSelector.viewed()
      .pipe(
        filter((viewed) => viewed),
        take(1),
        tap(() => this.navCtrl.navigateRoot('/home')),
      );
    const showHome$$ = showHome$.subscribe();
    this.subscriptions.push(showHome$$);
  }

  public ngOnDestroy() {
    this.subscriptions.forEach(subscription => subscription.unsubscribe());
  }

Fortunately, in my case, this code lives in “app.component.ts”, which means the component will never get destroyed or reinitialised unless the whole page is reloaded. So even without applying any fix, I am safe for memory leaks.

Thanks for reading.

I hope you find this article useful, and as usual, please leave me a comment here or DM me on Twitter if you have any questions.

Twitter: _pazel

Did you find this article valuable?

Support Parham by becoming a sponsor. Any amount is appreciated!