DEV Community

Cover image for RxJS can save your codebase
Mike Pearson
Mike Pearson

Posted on • Edited on

RxJS can save your codebase

YouTube

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 (See the demo here, but be careful... you might lose an hour):

Image description

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

Implementations

RxJS

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));
}
Enter fullscreen mode Exit fullscreen mode

Signals

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

    takeWhile((t) => t < 40),
Enter fullscreen mode Exit fullscreen mode

Becomes this with signals:

            if (count > 40) {
              this.clearInterval();
              this.state.set('blinking');
              this.runState.set({ count: 1, side: 'r' });
            }
Enter fullscreen mode Exit fullscreen mode

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):

<div [ngClass]="['bg dragon', dragonClass$ | async]"></div>
Enter fullscreen mode Exit fullscreen mode

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

  dragonClass$ = concat(
    this.runningClass$,
    this.blinkingEyesClass$,
    this.restingClass$
  ).pipe(repeat(Infinity));
Enter fullscreen mode Exit fullscreen mode

concat means "run these back-to-back". repeat(Infinity) 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

<div>{{count()}}</div>
Enter fullscreen mode Exit fullscreen mode

Non-Declarative

<div id="count">0</div>
Enter fullscreen mode Exit fullscreen mode
countElement.innerText = +countElement.innerText + 1;
Enter fullscreen mode Exit fullscreen mode

Variables

Declarative

const count = 0;
Enter fullscreen mode Exit fullscreen mode

Non-Declarative

let count = 0;

// ...

  count++;
Enter fullscreen mode Exit fullscreen mode

State

Declarative

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

const count = toSignal(count$);
Enter fullscreen mode Exit fullscreen mode

Non-Declarative

const count = signal(0);

// ...

  count.set(count() + 1);
Enter fullscreen mode Exit fullscreen mode

Effect Events

Declarative

const increment$ = incrementRequest$.pipe(
  mergeMap(() => server.increment()),
);
Enter fullscreen mode Exit fullscreen mode

Non-Declarative

const increment = createAction('INCREMENT');

// ...

increment$ = this.actions$.pipe(
  ofType('INCREMENT_REQUEST'),
  mergeMage(() => server.increment()),
  map(() => increment()),
);
Enter fullscreen mode Exit fullscreen mode

(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

const incrementRequest$ = fromEvent(incrementButton, 'click');
Enter fullscreen mode Exit fullscreen mode

Non-Declarative

const incrementRequest$ = Subject<void>();

// ...

  incrementRequest$.next();
Enter fullscreen mode Exit fullscreen mode

Benefits of declarative code

Focus

Image description

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 with Context

Image description

Image description

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

Avoiding Bugs with Context

Image description

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.

Comprehensibility

Image description

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

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:

const b$ = a$.pipe(delay(20));
Enter fullscreen mode Exit fullscreen mode

Image description

const b$ = a$.pipe(debounceTime(10));
Enter fullscreen mode Exit fullscreen mode

Image description

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:

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;
}
Enter fullscreen mode Exit fullscreen mode

This can be used like this:

count = signal(0);

delayedCount = delay(this.count, 3000);
Enter fullscreen mode Exit fullscreen mode

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 and in this YouTube video. 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

Top comments (26)

Collapse
 
maxime1992 profile image
Maxime

I was getting really tired of people who don't know how to use rxjs properly and making articles in the style of "rxjs is dead, long live Signals"...

Your article is absolutely on point and when I read such articles instead of desperately trying to summarize my thinking on a comment I'll just post a link to your article 👍.

Also subscribing because that is some quality content right there 👌.

Collapse
 
stealthmusic profile image
Jan Wedel

Ok, hot take:

A good framework/ library is flexible, powerful and hard to master.

A great framework/ library is flexible, powerful and easy to master.

In other words: if there are so many “people who doesn’t know to use rxjs properly”, maybe it’s not a problem of the people but of rxjs?

Collapse
 
maxime1992 profile image
Maxime

if there are so many “people who doesn’t know to use rxjs properly”, maybe it’s not a problem of the people but of rxjs?

Most people learn programming (or are being taught) using imperative programming. Which can be working very well in some cases.

When it comes to dealing with multiple asynchronous events, embracing reactive programming can help a ton, to reduce code complexity and keep the code concise. This has been demonstrated brilliantly in this article so I'm not going to give other examples.

But just like every tools out here, something that will give you extra power, requires extra efforts in the first place to learn it.

RxJS is seen as a complex library because people don't understand the basics of reactive programming before trying to use RxJS. It's like trying to fly a plane because you know how to drive a car. While the end goal to move from point A to point B is the same, one cannot simply fly a plane because he's got a driver licence. RxJS is no different. You may not need to fly a plane, which is totally fine. Mark my word on this. But don't expect it to be as easy as driving a car. It'll give you extra capabilities, but you will need more learning and practice. Nothing comes for free.

On the same note, if you want to carve a really complex shape on a piece of wood for example, you can either spend a 100h doing it by hand, for each pieces, or spend 200h learning CAD and how to use a laser cutter, and then being able to make 10 pieces an hour instead of one every 200h. It's all about what you want to achieve, if you plan on having complex projects or not, the energy you're willing to put in it before getting anything VS having something faster the first time but not the rest of the time, etc.

To be a lot more concrete on this, I've had a colleague a few years back that had never written any Javascript or Typescript in his entire life. He was mostly working in Java. He decided to help me out with some frontend work because there were more backend people than frontend devs, so he jumped on the project and discovered everything at once: Javascript, Typescript, Angular, Npm, Rxjs, Redux, Ngrx, ... This is usually a nigthmare scenario rigth? You could think it'd take months for him to reach a state he'd be able to contribute significantly to the codebase? Not at all. After barely 2 weeks, he was making excellent merge requests where I barely had anything to say. Involving complex state management and complex streams manipulation with RxJS. Why? How? Because he had studied IT for 8 years (phd) and knews all those patterns. He didn't know any of the syntax for the language nor any of the APIs for the different libraries. But he knew all the concepts. And just like you can easily drive a different car than yours when you rent one, he only had to learn a "surface API". Not all the concepts that these libs and frameworks relied on. Unit/integration/E2E tests, TDD, dependency injection, observables, promises, callback, threads, state management, data structures, separation of concerns, patterns, ... He knew all this. He didn't have to "learn" RxJS in any way. He only had to look in the documentation for the name of the operator he wanted to do something specific.

On the contrary, I think a lot of people try to fly a plane without even knowing how to drive a car in this scenario. I'm not trying to be rude in any way. I know there's a reality with deadlines and people put on projects they didn't create in the first place that are asked to take over and must deliver without necessarily having the time to be familiarised with patterns nor libraries. But I don't think libraries are the ones to blame here.

A great framework/ library is flexible, powerful and easy to master.

Agree. If you know the generic concepts of reactive programming (which is kind of a requirement here), I doubt you'll struggle to discover/learn/use RxJS. And if you know how to drive a car A, I doubt you'll struggle to use a car B. But if you have only used a bike so far, jumping in a car without taking lessons first because it also uses wheels may be problematic.

In other words: if there are so many “people who doesn’t know to use rxjs properly”, maybe it’s not a problem of the people but of rxjs?

I'll return the question from my own POV: Do you think everybody using RxJS has made the effort of learning the basics of reactive programming before using a specific implementation of that pattern?

Thread Thread
 
stealthmusic profile image
Jan Wedel

Rxjs is not some esoteric lib that you need to use for something very complex once in a life time.

So there are a LOT of people that potentially need to use it.

So let’s stick with your metaphor. What you want is that almost everyone needs to learn how to fly a plane and I’m saying it should be enough to drive a car in 99.9% of the cases.

Do you think everybody using RxJS has made the effort of learning the basics of reactive programming before using a specific implementation of that pattern?

I think it’s much more than just „the basics“ (see flying a plane) and I think they should not need to learn it.

I cannot speak for everyone but I would consider myself as a reasonably experienced developer. I know how to build hardware, program assembler, know how OSes work. I build asynchronous embedded frameworks that are still used in production and I think that I understand pretty well how rxjs works down to its implementation. But I still think rxjs is not the right abstraction for the masses. The problem is that there is no alternative for JS at the moment (imparative style with callbacks and state variables e.g. is WAY worse and a hell to maintain), probably also due to the lack of property language features that allow you to natively do things that you can - as of now - only do with rxjs.

Thread Thread
 
maxime1992 profile image
Maxime

The point of my metaphor with the car and plane can easily be hijacked (no pun intended). I don't think the learning gap between driving a car and flying a plane is fair enough to compare with RxJS to be honest. I was taking an extreme example just to illustrate the fact that any extra power comes with a given cost.

I think it’s much more than just „the basics“ (see flying a plane) and I think they should not need to learn it.

I disagree here. It's like thinking someone could just jump into 3D programming because of WebGL and how much easy they've made it to do that kind of work with Javascript. Because one knows Javascript, doesn't mean he'll be able to implement straight away a 3D view. Sure! You can use a framework that wraps some of the complexity and hides it away. But if you know nothing about shaders, vertices, matrices etc... You'll end up with renders that behaves in a way you don't fully understand and you'll be blocked for advanced behaviours. The same applies to RxJS. One can just jump into and use a map or a filter quite easily. But if someone isn't familiar with reactive programming or even just functional programming in general, he'll have a very limited scope at first. Understanding basic operators will be fine but higher order observables for examples won't make any sense at all. You can use the basics but not the most advanced parts. And maybe a lot of people don't need to because they don't have loads of async operations to manage. It's probably not 99% of the cases, but still a pretty decent amount I reckon.

But I still think rxjs is not the right abstraction for the masses

I don't fully disagree here I think.

The problem is that there is no alternative for JS at the moment

But that's part of my point. RxJS solves TODAY a really big issue that would be an absolute nightmare to manage without it. If someone finds a simpler way to achieve the same or more, I'm all hear.

And to clarify my point, I'm absolutely not against using Signal. Signal has a seat to take in Angular world. For synchronous things. While observables should still be used for more complex async ops.

My only point with my original comment was to really say that when Signal came out I saw plenty of blog posts saying RxJS was dead because of Signal and with loads of poor examples and comparisons between the 2. See my answer here to one of them to better understand my POV.

Thread Thread
 
stealthmusic profile image
Jan Wedel

Ok, I guess we are not that far apart but viewing it from different angles. 😉

And to clarify my point, I'm absolutely not against using Signal.

Let me also add here, I never said that Signals is in any way better than rxjs or will replace rxjs. My only hope that it is easier to handle due to its lower complexity (computed, effect, set etc vs. dozens of pipe operators) in cases where it makes sense to use signals (which will not be possible everywhere).

Collapse
 
sam_barnum_5d3061456be2bb profile image
Sam Barnum

Is RxJS Useful? Absolutely. Could RxJS be made easier to learn, for those who have not mastered it? I would say yes.

If you have an Observable, what can you do with it? Many things, but you need to know which of the 100+ oddly-named operators to use, import them, and pass them to pipe(). Your IDE's autocomplete is no help here, you simply need to memorize all the operators. Once you choose an operator and want to check what it does, the documentation often shows nothing, because the same signature is overloaded, and your IDE (IntelliJ, for me) shows the docs from the first matching TypeScript signature, which is always an undocumented legacy implementation.

Compare this behavior with calling methods on an object. You hit . and autocomplete shows all the choices, with popular ones first. Not sure what "mergeMap" is? Hit a shortcut, and see a detailed description of the method.

I think this would help. A little. RxJS is just very complicated.

Thread Thread
 
mfp22 profile image
Mike Pearson

TypeScript has spoiled you! :)

RxJS used to do that, and the TC-39 proposal that recently gained momentum again has that again. But only for ~7 common operators.

RxJS is a good career investment though. It's worth learning.

Collapse
 
buffos profile image
Kostas Oreopoulos

I really do not understand why there is such a confusion.

Signals manage state synchronization. End of story.

Rxjs is mainly event manipulation - creation. State management comes as an effect to The event flow.

Signals can be manipulated with Rxjs operators, as any other object, since Rxjs just reacts to events.

Rxjs is like the a complicated phone center. For example we can compare a signal to a ringing phone
When the phone rings things happen to connected machines to the phone.

Let's call that reactivity(1)

Now let's have 3 phones. We can program what things do when each rings (reactivity(1)) or when 2 ring at the same time or some time apart (reactivity(2)).
And now we want trigger an action on more complex patterns.
We can write down those patterns, but it is tedious an tiring. Do we create a language that helps us compose those patterns. This is Rxjs

There is the only connection between those two.

Signals are great for the internals of angular. To manage more efficiently change detection.

Collapse
 
artydev profile image
artydev

Very informative, thank you

Collapse
 
spaceninja151 profile image
Christian Knoll

Your clean code reads like a nice story! Great article!

Collapse
 
robertadamsonsmith profile image
Robert Smith

Very interesting article. Thank you for writing it.

I hope this won't come across as combative, but I did try writing the same thing in 5 lines of js, using a single 15 line utility function (it does actually use signals in the Svelte 5 preview, but the behaviour is all generally simple enough that the framework isn't especially important to understand).

To me, that wins in terms of least lines of code, and just most obvious behaviour. So, in this very specific example, RxJS looks like a lot of extra code and cognitive overhead. I get that the example code that you provided is necessarily simple enough for people to understand though, which makes this sort of comparison unhelpful.

I think you mentioned that the RxJS version shines when you need to later adapt the code, or the behaviour is more complex. It would be interesting to see what those sort of changes might be in order to prove that (I have a feeling that you are correct, but I have too little RxJS experience to know that for sure). What sort of things might need to be changed about the logo, so that it can be shown how RxJS provides a better initial approach which can adapt to these sort of changes?

Collapse
 
mfp22 profile image
Mike Pearson • Edited

Yeah here's another version with RxJS. It's what you did, but I replaced the 20-line utility with 11 lines of RxJS. I added a line in the svelte component to subscribe to the observable.

Edit: And it used concat, timer, map and repeat, which most RxJS devs would immediately understand. Preexisting utilities always win in my book, because you can already know them before encountering them on the next project.

Collapse
 
robertadamsonsmith profile image
Robert Smith

I agree that the utility function I wrote could be rewritten using RxJS, or with any number of other libraries, to a similar size/complexity, but that doesn't help me understand how RxJS can make the code I write better.

You've gone to a lot of effort writing these posts, and I apologise I've been overly critical. I will read up more on RxJS, and maybe it will click.

Thread Thread
 
mfp22 profile image
Mike Pearson

The code you wrote is mostly declarative. It's pretty good, and I would prefer it over the huge implementation I saw. If it was named and formatted as carefully as my original RxJS implementation, it would be about the same size as the RxJS version.

But it took me a minute to understand it. Like I said in the article, if you know RxJS, you already understand a lot of future apps. I wish concat was named series, but otherwise, I don't even think those 4 operators I used are complicated even if you've never seen RxJS before. They might seem like magic though; especially repeat(Infinity).

And to add play and pause, it would take 5 lines probably. I don't know. I thought of 2 ways of doing it but not in detail. Any idea how you'd do it with the async function? Rather than awaiting the timer, you could check if it's paused, then await an unpause event—how do you resolve a promise based on an event? Add an event listener that checks a callback that resolves the promise, and when the promise is created it sets the callback to its resolve function if the current state is paused. Otherwise it would resolve immediately. Anyway, that's basically an observable. Not sure how else you could do it.

Thread Thread
 
robertadamsonsmith profile image
Robert Smith

That is the sort of behaviour change that is interesting to look at, and will hopefully highlight something.

If it only needed to be able to pause/play, then I think that could be incorporated fairly cleanly into the async code with a custom timeout wrapper with pause/resume methods. That wouldn't make it possible to dynamically play the animation in reverse though, or jump to a set time in the animation, so would probably be a bit limiting in the long run.

I ended up separating the concern of time, from the concern of getting the frame state for a given time, which feels pretty clean, and means that I can pause/resume/reverse/fast-forward by just changing the timers speed property. That separation did require a rewrite of the utility code from before though (which perhaps the RsJS version wouldn't require), but only about 4 more lines of js overall (more if it was spaced out better and with comments).

Example here.

How would these changes be incorporated into the RxJS version?

I did try figuring it out myself, but I didn't get very far. I can see that timer() can take an extra Scheduler parameter, so maybe a custom scheduler is needed that can then be externally controlled to the LogoComponent itself? And then I think that concat() would need to be replaced with some custom function that could be dynamically reversable (for when playing in reverse). I don't think that jumping to a set point in time could be naturally achieved, but I don't have a good enough grip of RxJS to know if I'm just going about this all wrong, or if I'm just setting an unfair design requirement by adding reversing and time setting. Clearly I don't have a good enough understanding of RxJS here to go with the flow of it.

Collapse
 
patricknelson profile image
Patrick Nelson • Edited

Hey @mfp22! I'm not at all familiar with RxJS but was just curious. Long path lead me here (meandering from your latest "Honest, vulnerable rant"). Your rewrite demo didn't work at all for me (probably because Svelte 5 and its REPL is evolving still), so here's a tweak of it.

Other edits: I also found a bug in the code where value(t) was used instead of value(i) if you'll excuse that modification. I also had to explicitly import rxjs in order to get those functions. Finally, for some reason the observable wasn't working natively anymore so I imported another library to wrap that in a store so that it could be used again.

Collapse
 
mfp22 profile image
Mike Pearson • Edited

You didn't just change the async stuff. You rewrote the logic, removed variable names, and made it harder to understand. And the line width is 107 characters in the longest line, whereas in my RxJS code, the longest line was 57 characters. If my only goal was fewer line breaks, I could have done it in half the lines of code.

I knew the signal implementation could have been a lot smaller too. But it's a reasonable approach to structure it like a state machine. David K. Piano praised it.

The other thing to consider is that all of those RxJS utilities were already created for me. I didn't have to make any of them.

I'm just throwing this out there, but maybe some way to make it more complex would be to have it pause on mouse down and resume on mouse up. I'm still trying to figure out how you got it to do what you did though.

Collapse
 
robertadamsonsmith profile image
Robert Smith

I do take your point that the line length in the rewrite I gave is a bit long - it does need a few carriage returns added for sure!

Your implementations generate CSS class names, but to keep model and interface concerns separate, I did do things slightly differently there. I could have still generated class names subsequently from the model, and if I added extra variables and things I'm sure it would have all been closer in length to the RxJS version.

I don't quite agree about your code being easier to understand though. For me, a single clean purpose built utility function is far easier to reason about than the interaction of half a dozen RxJS functions. As somebody experienced in RxJS you will inevitably feel quite different about that.

I am genuinely interested though to find out how the RxJS version can adapt to changes. I assume that with the RxJS code I could make certain changes to it that would allow some more sophisticated behaviours that the more basic approach I showed wouldn't adapt to so easily. RxJS is popular, and I'm sure for good reason. I realise that the benefits of a certain coding style can't always be apparent in examples that are small enough to be quickly understood, and so I'm just trying to learn.

Collapse
 
stealthmusic profile image
Jan Wedel

You spoiled about that article and I was looking forward to it already. ☺️

Unfortunately / luckily I’m on vacation and it’s a bit hard to write on mobile. So bear with me.

So first thanks for the article! Although I tried signals before, I never though about an rxjs comparison. So it’s great to see that in action.

That said, here are some points I’d like to mention:

  1. Less lines of code is not necessarily >better< code.

Let’s take this example:


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}`;
Enter fullscreen mode Exit fullscreen mode

This is short but anything from readable or maintainable. I would refactor a lot of this code into readable methods which would increase the number of lines of code. On the other hand, in the signals example, there is a lot of duplication that could be easily refactored into reusable methods which would decrease the lines of code.

So, just looking at the LOC does help making code readable and is not particularly fair in this example IMO.

  1. Abstraction

This is exactly my problem with rxjs. In your example:


if (count > 40) {
              this.clearInterval();
              this.state.set('blinking');
              this.runState.set({ count: 1, side: 'r' });
            }
Enter fullscreen mode Exit fullscreen mode

Despite the fact that this could easily refactored in a domain specific single line method plus the if, people that learn JS know what an if clause does and how to use it. With Rxjs, they have to learn a second DSL on top, so the need to know what takeWhile does and how it’s implemented. So it’s a mental overhead.
Yes, if you know and master all the operators, you could write shorter code but is it maintainable also for new devs joining the team?

I strongly believe that any code is harmful and no code is the best code, but I am more that happy to trade in more readable maintainable code for having less code.

And I still would like to see good async primitives in JS. If you look at Erlang/elixir eg, they did a great job on asynchronous handling with language primitives.

I give you a point regarding declaring the behavior and not caring about when/how it’s executed.

I will stop here as it’s gets hard to read you article and write the comment here 😅

I’d really like to have a discussion and would love to hear your thoughts on this!

Collapse
 
mfp22 profile image
Mike Pearson
  1. Less lines of code is not necessarily >better< code

I addressed this in the article. I said " More code * usually * takes more time to understand, leaves more room for mistakes and is harder to change."

When it's 4x the amount of code? There is no reasonable debate here.

This is short but anything from readable or maintainable

Did you see the signal implementation? I made very few modifications to the synchronous logic. I think I improved it by creating more descriptive variable names. It's just super spread-out with imperative wrapper functions so it doesn't seem as daunting. But it's all still there. It's incredible someone can fool you into thinking something is easier by spreading it out and giving it to you in little bite-sized pieces instead of all in once place. Why don't you just read the RxJS version one line at a time and take a break in between?

I would refactor a lot of this code into readable methods

The way to improve both of these implementations would be to use enums instead of hardcoded values. What is 3? What is 4? I think they're leg positions, but I can't remember. So it should be ? LegPosition.Up : LegPosition.Down instead. And the signal version, I am not sure what repeated code you saw, but I'll take your word for it. 3.5x as much code, or 3x, or 2.5x, or 2x, or even just 1.5x, is still worse. If it was 20% more code, we could have a debate.

a second DSL on top

RxJS is not a DSL. JSX is a DSL. RxJS is JavaScript functions.

And I haven't written an if statement in a long time (except in advanced cases). Ternaries do the same thing but declaratively. What if beginners should learn declarative patterns instead of the typical spaghetti syntax introduced in every tutorial?

This is exactly my problem with rxjs.

This is exactly why I love it. I would rather use an existing function than reimplement it, every time. Is it harder to understand something that exists than re-implement it from scratch? That's extremely naive. You'll forget to clear a timeout. Or something. It always happens in imperative code.

Stop making extra work for yourself. Learn tools that do the work for you.

the need to know what takeWhile does and how it’s implemented

What behavior could takeWhile possibly implement? Is it really hard to imagine? I'd love to hear what possible alternate meanings it could have. Do you think it calls an API or sets a timeout? Or maybe it takes... while something is true. That's barely more complex than an if statement.

I am more that happy to trade in more readable maintainable code for having less code

You would rather have unmaintainable code if it means you don't have to pay a one time cost of learning RxJS operators. There's a reason I titled the article "RxJS can save your codebase". This signals version needs to be salvaged.

If you don't believe me, I would challenge you to a contest: Someone you trust implements a heavily async feature (like this) without RxJS, in some imperative style you like. You are given 3 tasks: Explain the behavior, find a bug, and alter the behavior. Meanwhile, I will receive the same codebase implemented 100% declaratively, and receive the same challenge. Is there any question in your mind who would be more productive?

I really can't say it better than I already did in the article. A developer who invests in learning RxJS will be far more productive than one who doesn't. It's not like each project needs an entirely new set of operators. It's always the same core ~20 operators, or slight variations of them (like takeWhile rather than take).

My frustration must be coming through in this comment. Every time I put more effort into clarifying the value of RxJS, someone puts in even more effort in misunderstanding it. I put the answers to most of your criticisms in the article itself, because I've heard them a hundred times, and it's as if the words were invisible to you.

Do we need a more extreme example? If I found an example that took 5x the amount of code to do imperatively, would that be enough? 8x? 10x? 20x?

Collapse
 
stealthmusic profile image
Jan Wedel

Did you see the signal implementation?

Ok, let’s leave out signals for a moment here. I don’t have any prod code experience so far. I am not advocating for signals over rxjs or anything.

RxJS is not a DSL.

I'm pretty sure it is. The base language is JS/TS and it’s domain is reactive programming/ async event handling.

startWith(0),
takeWhile((t) => t < 5),
map((t) => `l${t % 2 === 1 ? 1 : 2}`)
Enter fullscreen mode Exit fullscreen mode

The non-reactive “equivalent” is probably a for loop in JS. Obviously, this cannot be replaced but it shows that rxjs is a language on top of JS, it’s not just an abstraction.

You would rather have unmaintainable code if it means you don't have to pay a one time cost of learning RxJS operators.

No, but it is not a one time cost. Rxjs is just way to complex to learn it in advance. Plus, I don’t actually want to see any technical code on that level of abstraction. I would usually hide logic (ifs and loops etc) in methods that are domain specific. Similar to that, I don’t want to see “takeWhile” and “start” etc. this is technical code. IMO this should completely vanish until the lowest level of abstraction in your code. Rxjs makes it really hard to write code that expresses the functional aspect because you’re forced to put the top level in some pipe operators.

You are given 3 tasks: Explain the behavior, find a bug, and alter the behavior. Meanwhile, I will receive the same codebase implemented 100% declaratively, and receive the same challenge. Is there any question in your mind who would be more productive?

I’ve seen as many bugs in rxjs code than in imperative code. However, rxjs code IME is a lot harder to debug or understand when something is a lot more complex and goes wrong. I’ve seen so many “why does it emit 3x instead of 1x” or “why doesn’t it emit at all”. People confuse the declarative part with the runtime part. People don’t know that wirhLatestFrom created new subscriptions just as one example.

Yes, if once know all of that after years of experience it might be fine. But it’s not a free lunch to get there.

Maybe I’m not seeing the light here.

But as much frustrated you seem about people misunderstanding you or rxjs I am frustrated about people blaming it on people not understanding it. See my other comment here.

Maybe there are lots of teams working with rxjs and not having those kind of problems. Then just keep on doing it.

Maybe there just no better way to do it, but for me, it is not the best way and I just have the feeling that rxjs is not the end in reactive programming. There must be a better way.

On the backend side, I’ve seen Spring WebFlux as an alternative reactive server framework in Java. We introduced one microservice whose domain seemed to fit best with reactive style. But it was both difficult implementing as well as maintaining the code. So after some years of experience, we decided to rewrite it again with imperative approach.

Thread Thread
 
yanxch profile image
Christian Ja

thanks for this discussion. on point.

every abstraction comes with a cost, be it from rxjs or from a domain specific method written in imperative/oop/whatever

my take would be:
we should learn functional programming first, before diving into reactive concepts.

eg. using pure functions alone really improves code quality

Collapse
 
robbiespeed profile image
Robbie Speed

The signals code here could be much improved to the point of being nearly identical to that of the rxjs example. This is more a showcase of the API that rxjs provides, signals can be extended to also provide a similar api. In this case all that would be necessary is a timer signal, then you could avoid the convoluted effect by using computeds.

Collapse
 
mfp22 profile image
Mike Pearson

Yeah I was getting to this near the bottom of the article. I haven't figured out how to implement completions with signals yet though. That would be required to figure out repeat(Infinity).

Collapse
 
jkf16m_jkf16m_e5d8238a4 profile image
Jkf16m (Jkf16m)

I think this is a great take.

It's a nice summary to have, Signals or react hooks, or whatever, are just the lowest level of a reactive API.

Rxjs is already a library that defines common use-cases for reactivity, in this case for streams and reactivity.

I think that's why rxjs is hard to really understand, if in imperative programming you have "=" "+=" "-" and logical operators...

in rxjs you have a bunch of operators that makes your observable behave in a completely different way.

When I first started using Rxjs, I didn't understand why there were operators for literally everything.

Like, some operators lets you repeat the result and others lets you "transform" the values.
And others just return more observables.

It's hard, but manageable.

Still, I can't grasp that stream diagram that is usually shown for rxjs.