RxJS can save your codebase
Tags: angular, signals, rxjs
Published: 2023-07-24 06:42:37

---
title: RxJS can save your codebase
published: true
description: Sometimes RxJS is the *perfect* tool for the job. If you've used it for the wrong job, you might never want to touch it again. But if you remain open-minded towards it, it can sometimes save your codebase from exploding into overly complex spaghetti-code.
tags: angular,signals,rxjs
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w4ba2jt5upv3qj0if3qp.png
# Use a ratio of 100:42 for best results.
# published_at: 2023-07-23 00:18 +0000
series: Signals vs RxJS
---

[YouTube](https://www.youtube.com/watch?v=fyQAzO9kdb0)

Sometimes RxJS is the *perfect* tool for the job. If you've used it for the wrong job, you might never want to touch it again. But if you remain open-minded towards it, it can sometimes save your codebase from exploding into overly complex spaghetti-code.

We've seen what happens when we use RxJS for synchronous reactivity. Now let's try to use *signals* to implement *asynchronous* behavior. We'll compare both RxJS and signal implementations of this dinosaur animation from the opening screen of the [Angular Tetris app](https://github.com/trungk18/angular-tetris) (See the demo [here](https://tetris.trungk18.com/), but be careful... you might lose an hour):

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/va5qj83q2u6bqp06yb5y.gif)

Let's look at the implementations first, then analyze them. 

## Implementations

### RxJS

```typescript
export class LogoComponent {
  runningClass$ = timer(0, 100).pipe(
    startWith(0),
    takeWhile((t) => t < 40),
    map((t) => {
      const range = Math.ceil((t + 1) / 10);
      const side = range % 2 === 0 ? 'l' : 'r';
      const runningLegState = t % 2 === 1 ? 3 : 4;
      const legState = t === 39 ? 1 : runningLegState;
      return `${side}${legState}`;
    })
  );

  blinkingEyesClass$ = timer(0, 500).pipe(
    startWith(0),
    takeWhile((t) => t < 5),
    map((t) => `l${t % 2 === 1 ? 1 : 2}`)
  );

  restingClass$ = timer(5000).pipe(
    startWith(0),
    map(() => 'l2')
  );

  dragonClass$ = concat(
    this.runningClass$,
    this.blinkingEyesClass$,
    this.restingClass$
  ).pipe(repeat(Infinity));
}
```

### Signals

```typescript
type State = 'running' | 'blinking' | 'resting';

type RunningSide = 'r' | 'l';

interface RunningState {
  count: number;
  side: RunningSide;
}

// ...
export class LogoComponent {
  private runState = signal<RunningState>({ count: 1, side: 'r' });
  private blinkingCount = signal(1);
  private state = signal<State>('running');

  className = computed(() => {
    switch (this.state()) {
      case 'running': {
        return this.getRunningClassName();
      }
      default: {
        return this.getBlinkingClassName();
      }
    }
  });

  private intervalRef = null as null | number;
  private setTimeoutRef = null as null | number;

  constructor() {
    effect(
      () => {
        switch (this.state()) {
          case 'running': {
            if (!this.intervalRef) {
              this.intervalRef = this.startRunning();
            }

            const { count } = this.runState();

            if (count > 40) {
              this.clearInterval();
              this.state.set('blinking');
              this.runState.set({ count: 1, side: 'r' });
            }
            break;
          }
          case 'blinking': {
            if (!this.intervalRef) {
              this.intervalRef = this.startBlinking();
            }

            if (this.blinkingCount() >= 6) {
              this.clearInterval();
              this.state.set('resting');
              this.blinkingCount.set(1);
            }
            break;
          }
          case 'resting': {
            this.setTimeoutRef = window.setTimeout(() => {
              this.state.set('running');
            }, 5000);
            break;
          }
        }
      },
      { allowSignalWrites: true }
    );

    inject(DestroyRef).onDestroy(() => {
      if (this.intervalRef) {
        clearInterval(this.intervalRef);
      }

      if (this.setTimeoutRef) {
        clearTimeout(this.setTimeoutRef);
      }
    });
  }

  private startBlinking() {
    return window.setInterval(() => {
      this.blinkingCount.update((count) => count + 1);
    }, 500);
  }

  private startRunning() {
    return window.setInterval(() => {
      this.runState.update(({ count, side }) => {
        const newCount = count + 1;

        return {
          count: newCount,
          side:
            newCount === 10 || newCount === 20 || newCount === 30
              ? side === 'r'
                ? 'l'
                : 'r'
              : side
        };
      });
    }, 100);
  }

  private getRunningClassName(): string {
    const { count, side } = this.runState();
    const state = count === 41 ? 1 : count % 2 === 0 ? 3 : 4;
    return `${side}${state}`;
  }

  private getBlinkingClassName() {
    const state = this.blinkingCount() % 2 === 0 ? 1 : 2;

    return `l${state}`;
  }

  private clearInterval() {
    if (this.intervalRef) {
      clearInterval(this.intervalRef);
    }
    this.intervalRef = null;
  }
}
```

## Lines of Code

RxJS wins at this:

|      |   Lines of Code |
| ---- | --- |
| RxJS | 30  |
| Signals    |   123  |

More code usually takes more time to understand, leaves more room for mistakes and is harder to change.

## Level of Abstraction

The signals implementation is working on a lower level of abstraction. This RxJS code:

```typescript
    takeWhile((t) => t < 40),
```

Becomes this with signals:

```typescript
            if (count > 40) {
              this.clearInterval();
              this.state.set('blinking');
              this.runState.set({ count: 1, side: 'r' });
            }
```

RxJS operators describe actual behavior. If you don't use them explicitly (or something equivalent), you will be redefining them implicitly in scattered, repetitive logic coupled to business logic.

A developer who understands RxJS operators automatically understands more potential behavior of features, so the cost of becoming familiar with a new codebase that uses RxJS is less than the cost of learning the equivalent codebase that doesn't use them.

Learning RxJS is a good investment for your career.

## Debugging

When something is wrong it is usually faster to directly inspect the thing that's wrong instead of making a guess about what code is related and trying to find the problem from there.

Imagine you wanted to know why the dragon was blinking for 2.5 seconds. If you directly inspected the dragon, you would find this (in the RxJS implementation):

```html
<div [ngClass]="['bg dragon', dragonClass$ | async]"></div>
```

If you `Cmd Click` on `dragonClass$`, you see this:

```typescript
  dragonClass$ = concat(
    this.runningClass$,
    this.blinkingEyesClass$,
    this.restingClass$
  ).pipe(repeat(Infinity));
```

[`concat`](https://rxmarbles.com/#concat) means "run these back-to-back". [`repeat(Infinity)`](https://rxmarbles.com/#repeat) repeats the previous behavior `Infinity` times.

So now we already understand the main behavior, and we could Cmd Click on `blinkingEyesClass$` to go into more detail.

This is a very convenient debugging experience, and it's because RxJS enables declarative syntax for even asynchronous behavior.

## Declarative Code

Most of the benefits of the RxJS approach come from its declarative structure.

What does that mean?

### Declarative Code Definition

Declarative code is where each declaration completely defines a value across time. The answer to the question "Why is this thing behaving this way?" is always answered in its declaration, not somewhere else. 

Non-declarative code only initializes values, then leaves it up to scattered imperative code to define subsequent behavior. To answer the question "Why is this behaving this way?", you have to find all references to the variable, understand the context of each of those references, then determine if and how those references are controlling that variable.

### Examples of Declarative vs Non-Declarative Code

#### UI

Declarative

```html
<div>{{count()}}</div>
```

Non-Declarative

```html
<div id="count">0</div>
```

```typescript
countElement.innerText = +countElement.innerText + 1;
```

#### Variables

Declarative

```typescript
const count = 0;
```

Non-Declarative

```typescript
let count = 0;

// ...

  count++;
```

#### State

Declarative

```typescript
const count$ = increment$.pipe(scan(n => n + 1, 0));

const count = toSignal(count$);
```

Non-Declarative

```typescript
const count = signal(0);

// ...

  count.set(count() + 1);
```

#### Effect Events

Declarative

```typescript
const increment$ = incrementRequest$.pipe(
  mergeMap(() => server.increment()),
);
```

Non-Declarative

```typescript
const increment = createAction('INCREMENT');

// ...

increment$ = this.actions$.pipe(
  ofType('INCREMENT_REQUEST'),
  mergeMage(() => server.increment()),
  map(() => increment()),
);
```

(Note: Technically it's the store itself that's non-declarative, being modified by `store.dispatch`. But effectively each action is an event stream, and this code is defining when it gets dispatched.)

#### UI Events

Declarative

```typescript
const incrementRequest$ = fromEvent(incrementButton, 'click');
```

Non-Declarative

```typescript
const incrementRequest$ = Subject<void>();

// ...

  incrementRequest$.next();
```

### Benefits of declarative code

#### Focus

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pzbkrndb64oq8d2u1h81.png)

Declarative code allows behavior to be self-contained and isolated. When you're working on one specific feature or behavior, you don't have to worry about imperative statements elsewhere changing it, so you can ignore the entire rest of the world and just define that one thing.

#### Debugging

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nzh31gf5uawxp8atzqmg.gif)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j3ebrxohukbghf13uhob.gif)

When behavior is being controlled from one place, rather than from many places, bugs are easier to track down.

#### Avoiding Bugs

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6avyaoe29sjzz5imgk7c.gif)

It's easier to avoid bugs when you can reference all relevant context when editing code that defines its behavior. This is the reason Facebook invented Flux for state management.

#### Comprehension

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/39p9fpbdciasryq2ahl6.png)

It's easier to quickly understand code when it's organized by what it does rather than by when it's executed. Callback functions contain imperative code that runs at the same time (mostly) but controls unrelated states, whereas reactive declarations organize code by the actual behavior they define. 

### Declarativeness in RxJS vs Signals Only

I color-coded the RxJS and signals-only implementations by what state/behavior the code is defining/modifying:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oi6ttjet8yt94edfcwxq.png)

The signals implementation is not very declarative. This is because signals and computeds enable declarative code only for synchronous behavior. When time enters the equation, you need an `effect`, which, like all callback functions, is a container for imperative code. Since this feature involves a lot of timing behavior, it requires a lot of scattered imperative logic inside an `effect` and a few other places.

RxJS operators are currently the only comprehensive way to enable declarative code for asynchronous behavior. The only way to declare an asynchronous relationship between 2 variables is to define _how_ they are related, which involves the logic contained in the operators.

For example, `delay` and `debounceTime` are just 2 different ways in which a reactive asynchronous relationship can be defined:

```typescript
const b$ = a$.pipe(delay(20));
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wn2bj82a6anoogck1aor.png)

```typescript
const b$ = a$.pipe(debounceTime(10));
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/axk9uwqr774si7q2vbk3.png)

## Signal Operators

Could there be a way to define reactive asynchronous relationships without RxJS operators? RxJS operators are just functions that take in observables and return different observables. Could we write functions that could take in signals and return different signals and call them "signal operators"? 

I think we can. 

Let's try `delay`:

```typescript
export function delay<T>(inputSignal: Signal<T>, t: number) {
  const outputSignal = signal(undefined as undefined | T);

  effect(() => {
    const value = inputSignal();
    setTimeout(() => outputSignal.set(value), t);
  }, { allowSignalWrites: true });

  return outputSignal;
}
```

This can be used like this:

```typescript
count = signal(0);

delayedCount = delay(this.count, 3000);
```

This works, and `delayedCount` is 100% declarative syntax! We abstracted away the `effect` usage and imperative code, and enabled a declarative API. 

This is an extremely simple example. Something much more sophisticated is required to achieve what the dozens of RxJS operators achieve. 

So, my advice is this: 

> When you think you might need to write an effect for a signal, first try instead to
> 1. reorganize the code to use a `computed` 
> 2. use an existing signal operator
> 3. write a new custom signal operator
> 4. convert to an observable with `toObservable` and use an RxJS operator

And only as a last resort, use `effect` directly.

Currently you will have to always either use RxJS operators, or write your own signal operators. But I think I am going to work on a library for signal operators. I explained the full reasons in [this tweet thread](https://twitter.com/mfpears/status/1676863498599665666) and in [this YouTube video](https://www.youtube.com/watch?v=W4tpgEXYKyQ). Follow me on Twitter for updates :)

## Conclusion

This example was almost completely asynchronous. What you will see in the real world will land somewhere on the spectrum between synchronous and asynchronous.

The more asynchronous your app is, the more RxJS can help you keep your codebase clean, bug-free and easy to debug. 

Just remember:

Avoid `effect`.

1. Use `computed`.
2. Use a signal operator.
3. Create a signal operator.
4. Use RxJS.

---

Thanks for reading! Follow me on Twitter for more frequent content: https://twitter.com/mfpears