Replacing RxJS With A State Machine In JavaScript

By admin
Replacing RxJS With A State Machine In JavaScript

I have a lot of trouble working with reactive streams. It’s just not how my brain works best. Give me a simple event-stream, and I can mostly hold that in my head. But, start to combine streams together, and my brain freezes up like a deer in headlights – my eyes darting from operator to operator, desperately trying to build-up a mental model of what is actually happening. As such, I recently removed

RxJS
PERSON

from an application and replaced the reactive streams with state machines. It’s definitely more code; but, I find it easier to reason about and maintain.

Run this demo in my

JavaScript Demos
ORG

project on GitHub.

View this code in my

JavaScript Demos
ORG

project on GitHub.

ASIDE: My desire to replace

RxJS
PERSON

in this particular context was driven primarily by the sheer amount of "vendor" code that was being pulled-in. This is an old app with no tree-shaking and it was pulling in literally

hundreds-of-kilobytes
QUANTITY

of

RxJS
PERSON

and only being used in

two
CARDINAL

places. My main objective was in reducing the vendor payload. Making the code easier to reason about (for me personally) was a side-effect.

I won’t reproduce the entirety of the

RxJS
PERSON

code here, but I’ll give you a high-level sense of how the streams were being combined. The context for this code is a mouse-drag observer (following by some window-scrolling, not shown). Hopefully I didn’t break the code here too much by trying to pare it down.

// Get the

three
CARDINAL

major events var mouseup = Rx.Observable.fromEvent(document, ‘mouseup’); var mousemove = Rx.Observable.fromEvent(document, ‘mousemove’);

var mousedown
PERSON

= Rx.Observable.fromEvent(dragTarget, ‘mousedown’); var intervalSource = Rx.Observable.interval(60, Rx.Scheduler.requestAnimationFrame);

var mousedrag
PERSON

= mousedown.flatMap(function(md) { return intervalSource .takeUntil(mouseup) .withLatestFrom(mousemove, function(s1, s2) { return s2.clientY; }) .scan(md.clientY, function( initialClientY, clientY ) { return( clientY – initialClientY ); }) ; }); this.subscription = mousedrag.subscribe(function( delta ) { console.log( "Drag delta from origin:", delta ); });

I’m sure – or, rather, I assume – that if you were proficient in

RxJS
GPE

, this code would make sense at a glance. But, I’m not very familiar with the

RxJS
ORG

operator APIs (which is part of why I find

RxJS
PERSON

so confusing). So, when I look at this code, it’s not immediately obvious how it all fits together. Clearly, it has something to do with mouse movements; but, if I had to describe the overall intent of the code, it would take me a while to construct a meaningful mental model.

After adding a number of console.log() statements to these stream callback, I was able to piece together the general control flow:

User mouses-down. We start an interval stream that emits an event ever 60ms. We read from the interval stream until the user mouses-up. When the user moves their mouse, we emit an event with the current mouse position. We combine the interval event with the mouse movement event and calculate the distance the mouse has moved relative to the original mouse-down event.

In order to translate this

RxJS
PERSON

into code that I could understand and maintain more easily, I started to think about the states that these streams represent. I came up with:

Default State : The user is just sitting there staring at the screen.

Pending State : The user has moused-down, but hasn’t moved their mouse yet. At this point, we don’t know if they are "clicking"; or, if they are about to start "dragging".

Dragging State: The user is moving their mouse while the button is still pressed.

None of these states exists in isolation. The overall interaction works by transitioning from

one
CARDINAL

state to another based on some sort of event:

Default → mousedown → Pending

→ → Pending → mouseup → Default

→ → Pending → mousemove (beyond threshold) → Dragging

→ (beyond threshold) → Dragging → mouseup → Default

There are all manner of libraries out there for defining and consuming state machines. But, for this exploration, I’m going to keep it simple. Each state is defined by a series of Functions whose names are prefixed with the current state. For example, default_setup() , is the initialization method for the "Default" state.

Each state has both a setup and a teardown method that is called when entering and exiting a given state, respectively. So, the

Default
ORG

state has both a default_setup() and a default_teardown() method. Each state defines its own event-handlers, which is how

one
CARDINAL

state knows when and why to transition to the next state.

The following code is my attempt to recreate the RxJS event streams with this

3
CARDINAL

-state system:

NOTE: In my approach, the "pending" state has to pass a given drag-threshold before moving into the "dragging" state. This constraint was not part of the RxJS event streams; but, I think it should have been. That said, it’s not obvious to me how I would have added it to the given

RxJS
PERSON

code.

<!doctype html> <html lang="en"> <body> <h1> Replacing RxJS With A State Machine In JavaScript </h1> <p> If you mouse-down and then start dragging (vertically), you get console logging. </p> <script type="text/javascript"> default_setup(); // — // DEFAULT STATE: At this point, the user is just viewing the page, but has not // yet interacted with it. Once they mousedown, we’ll move into the pending state. // — function default_setup()

{ console.info
PERSON

( "

Default: Setup
WORK_OF_ART

" ); document.addEventListener( "mousedown",

default_handleMousedown
PERSON

); } function default_teardown()

{ console.info
PERSON

( "

Default: Teardown
WORK_OF_ART

" ); document.removeEventListener( "mousedown",

default_handleMousedown
PERSON

); } function default_handleMousedown( event ) { event.preventDefault(); default_teardown(); pending_setup( event.clientY ); } // — // PENDING STATE: The user has moused-down on the page, but we don’t yet know if // they intend to drag or just click. If they do start to drag (and pass a minimum // threshold), we’ll move into the dragging state. // — function pending_setup( clientY ) { console.info( "

Pending: Setup
WORK_OF_ART

" ); document.addEventListener( "mouseup", pending_handleMouseup ); document.addEventListener( "mousemove", pending_handleMousemove ); pending_setup.initialClientY = clientY; } function pending_teardown()

{ console.info
PERSON

( "

Pending: Teardown
WORK_OF_ART

" ); document.removeEventListener( "mouseup", pending_handleMouseup ); document.removeEventListener( "mousemove", pending_handleMousemove ); } function pending_handleMouseup( event ) { pending_teardown(); default_setup(); } function pending_handleMousemove( event ) { // Only move onto next state if dragging threshold is passed. // — // CAUTION: This concept was not present in the

RxJS
PERSON

version; but, I think it // should have been. And, in the state-based approach (for me) it is easier to // reason about this update using states vs. streams. if (

Math.abs
NORP

( pending_setup.initialClientY – event.clientY ) >

10
CARDINAL

) { pending_teardown(); dragging_setup( pending_setup.initialClientY ); } } // — // DRAGGING STATE. // — function dragging_setup( clientY ) { console.info( "

Dragging: Setup
WORK_OF_ART

" ); document.addEventListener( "mousemove", dragging_handleMousemove ); document.addEventListener( "mouseup", dragging_handleMouseup ); dragging_setup.initialClientY = clientY; dragging_setup.currentClientY = null; dragging_setup.timer = setInterval( dragging_handleInterval,

60
CARDINAL

); } function

dragging_teardown
PERSON

()

{ console.info
PERSON

( "

Dragging: Teardown
WORK_OF_ART

" ); document.removeEventListener( "mousemove", dragging_handleMousemove ); document.removeEventListener( "mouseup", dragging_handleMouseup );

clearInterval( dragging_setup.timer )
ORG

; } function dragging_handleMousemove( event ) { dragging_setup.currentClientY = event.clientY; } function dragging_handleMouseup( event ) {

dragging_teardown
PERSON

(); default_setup(); } function dragging_handleInterval() { console.log( "Drag delta from origin:", (

dragging_setup.currentClientY – dragging_setup.initialClientY
ORG

) ); } </script> </body> </html>

So, we’ve gone from

21
CARDINAL

-lines of

RxJS
GPE

event streams up to

123
CARDINAL

-lines of

State Machines
ORG

. That’s almost 6x the amount of code. It might seem like we’re moving in the wrong direction. But – for me personally – when I look at this code, it’s much easier to understand. Furthermore, I don’t even have to fully understand all of the code at

one
CARDINAL

time in order to think about effectively; all I have to do is look at the current state and think about how it transitions to the next state.

If we run this code in the browser and drag the mouse around, we get the following output:

As you can see, we’re logging the setup and teardown events for each state. And, we’re logging-out the delta calculated during the dragging state. This state machine code works as intended.

This is not an argument against

RxJS
PERSON

. I know that many people absolutely love

RxJS
PERSON

and feel that it greatly simplifies the code that they write. I’m just not one of those people. Reactive event streams overload my brain. I find it much easier to think in terms of "states"; and, I don’t mind that it takes more code to make that happen.

Epilogue on Unsubscribing

In the

RxJS
PERSON

version of the code, a subscription object is created. When the current application View is "destroyed", that subscription object can be "disposed", which will, in turn, automatically stop all the upstream events from firing. That’s pretty nice!

In my state machine approach, when the current application View is "destroyed", I would simply call all

three
CARDINAL

of the *_teardown() methods (since I might not know which state is currently active). This is slightly less elegant; but, again, I’m OK with that.

Want to use code from this post? Check out the license.

Enjoyed This Post? ❤️ Share the Love With Your Friends! ❤️ Tweet This Great article by @BenNadel – Replacing RxJS With A State Machine In JavaScript https://www.bennadel.com/go/4519