How can I help my team love RxJS?

Michael Pearson
10 min readFeb 19, 2018

--

Understanding cause and effect is at the core of understanding any application, and reactive programming destroys the ability to see it clearly. But it’s also really powerful. So how can we help our fellow developers enjoy working with it?

Not everyone likes reactive programming. Many developers prefer imperative programming because it is simple and direct. If you click a button and the data in 3 places on the page needs to change, then… let’s grab those 3 places and get that text changed!

With more and more people using Redux and RxJS, many applications are becoming more difficult to understand, debug, and develop. These technologies help prevent bugs arising from inconsistent state, but the tradeoff is the huge amount of indirectness they introduce.

Take this example: In a chat application, a developer might see that when they click a “Leave Chat” button, the chat thread is removed from the list of chat threads in the view, but the websocket is not closed properly. Reactive architecture now gets in the way of understanding why this is happening. When the developer looks at the event handler for Leave Chat, they only see this.store.dispatch({type: 'LEAVE_CHAT', id}). After tracing the code around for a while, they finally arrive at the cause of the chat item disappearing from the list. It seems to them that identifying the mechanism of cause-and-effect in the process shouldn’t have been nearly that difficult. They were expecting something in the event handler like this.chatService.removeChatFromList({id}); so that they could also add this.chatService.ohAndCloseThisWebSocketPlz({id});. But it wasn’t there!

Are reactive programming advocates sadistic?

Maybe. But if you’re accustomed to reactive programming, you may have noticed that the bug in that example would not have existed in the first place if the code had really been reactive. The chat list would have subscribed to the central list of chat id’s, as would the observables responsible for opening and closing web socket connections, and they both would have reacted at the right times. The action handler wouldn’t have needed to remember both.

Doing everything imperatively means making sure you have accounted for every possible combination of effects that may result when you handle an action. Imperative programming may be direct and easy to follow, but once an app gets to a certain level of complexity, the chance is very high that developers will forget little things here and there, and inconsistent state begins to account for the majority of bugs.

Imperative code can result in unexpected consequences as well. An obscure feature might suddenly change state that another feature depends on, or state might be mutated in the wrong order, ripping open a portal to hell and requiring us to collect keycards of various colors to find a portal back.

Keycards of various colors

But most developers still strongly prefer direct, imperative code. So what do they do when the mandate comes from above to use this indirect Redux/RxJS architecture?

The most likely outcome is that developers will start to see state and the store as simply an extra step in the process of making things happen. So they start making state trees like this:

// Do not do this:
interface State: {
showModal: boolean;
loadItemDetails: boolean;
showLoadingIcon: boolean;
cancelRequest: boolean;
hidePopup: boolean;
navigateToPage: string;
cleanRoom: boolean;
goOutside: boolean;
doEverything: boolean;
engageInPhilanthropy: boolean;
}

State effectively becomes an awkward vehicle for commands. But it’s actually supposed to be a snapshot of information from which one and only one view can be generated. So where does that put us? Worse than we were with plain imperative programming, because not only do we now have all of this ridiculous Redux/RxJS machinery everywhere, but we still have to remember the effects of all these commands.

(Note: RxJS can be used without Redux of course, but without Redux you are probably still going to have the problem where commands get issued as observable values if you aren’t coding reactively.)

It’s very difficult to change from an imperative to a reactive frame of mind, and so you really have to want to. Many developers are not interested in thinking reactively, because they have not yet experienced much pain from inconsistent state, or perhaps they haven’t noticed that inconsistent state is the commonality between many of the bugs they have encountered.

When to choose reactive programming

Since it is counterproductive to push for reactive programming when the motivation is lacking, reactive programming seems to make sense in these situations:

  1. The value of reactive programming in the project is self-evident enough that the team members all desire to use it, despite its drawbacks
  2. The project is moderately suited for it, the team is not particularly opposed to it, and you have good ways to ensure reactive patterns (such as having only unique state in the store, using selectors for derived data, and avoiding local state)
  3. The cost of reactive programming can somehow be reduced so that even teams working on simple projects will want to use it

Since reactive programming can dramatically simplify complex applications, the 3rd situation is ideal in my opinion. If we could somehow reduce the cost of reactive architecture, teams will be able to adopt it while their apps are still small, before they lose massive amounts of resources chasing fires caused by shared, mutable state.

Decreasing the reactive cost

Coding reactively increases the difficulty of both solving a problem the first time and understanding an existing solution. Fortunately, many in the community are working on tools to address these challenges. For the rest of this post I would like to share some of what I’m most excited about, and also some of my own thoughts on how we might make it easier to understand and create reactive code.

Understanding Reactive Code

The key to understanding reactive code is to bring back the transparency of cause and effect in the application. Here are some tools that I believe do a great job at this:

  • Redux-devtools. One source of indirection in Redux apps is actions, which separate what happened from how state is supposed to change (state “reacts” to actions). Redux-devtools reconnects these concepts by showing how actions change state. Here is a nice video showing what it can do.
  • Andre Staltz has created a neat visualization tool for observables in his JS library, CycleJS. You can watch data flowing through your app at whatever speed you choose. I would love to see something like this for RxJS apps.
  • Inspired by Andre Staltz and others, this website allows you to visualize any observable chain. I think it would be amazing as a VSCode extension.

These are awesome tools, and there are still many opportunities to improve the transparency of reactive code. If you have any ideas, please leave a comment below and maybe you or someone can pick up an idea and run with it.

Solving Reactively

The difficulty with solving a problem reactively is that you need to keep so many things in your head that you quickly overflow your capacity and have to start over, maybe several times before you finally figure it out. It feels similar to when someone asks you to solve a math problem like this in your head:

32058
X 17
-----

To remember all the numbers alone is difficult, but then to also remember the intermediate steps and then calculate the interactions between the numbers is just more than most people are able to do.

So it’s a good thing that doing it in your head is not the only way to solve this problem. There is an easier method that involves writing down several small steps. This process moves the main problem away from the limitations of working memory to a more reliable process that can be learned and taught.

Is there a similar process for coding reactively?

Let’s take an ordinary example and compare the imperative and reactive solutions, and see if we can’t extract a process from the reactive solution that we could use to help us solve more complex problems.

Basic Example: Asynchronous Autocomplete

Most developers are pretty familiar with this example. A user types some text in an input, a filtered list of data containing the string they typed is fetched, and the results are shown to the user.

The imperative solution is straight-forward. We start with the (change) event handler. This function will take the search term as input, pass it into a function that fetches data, then when the data returns it will bind the data to the component class for the template to display. Here is an example implementation of this:

Autocomplete implemented imperatively

Great. That wasn’t too hard.

The reactive solution is harder to describe, but this was my thought process: The data needs to be fetched after a user types, so the observable that fetches data is watching for user input. User input will be a Subject that has the .next method called in the (change) event handler, so it looks like the data fetching method will have to switchMap off of that subject. But that stream should be assigned as a property on the component so it’s available for the async pipe, so that is where our code really starts. Let’s write all that down before we forget anything:

Autocomplete implemented reactively

That wasn’t too bad. And since this is reactive, we can now turn off change detection for this component, and we won’t be downloading data for previous search terms. This ensures that the results will never get out of sync with the search term. (Here’s the StackBlitz project with the 2 implementations.)

Could we have come to this solution a little more smoothly, without having to keep so much in our mind at once?

When we compare these two solutions side-by-side, we can see that the imperative and reactive solutions associate different steps in the process together:

Imperative programming associates the event to the immediate effect, whereas reactive programming associates the final effect to its immediate source of data. Ben Lesh and others have said a few times that “thinking reactively” is like thinking backwards or upside-down. If you look at the reactive solution and start at the end first, you’ll notice that the public data$ = part could have been written without even considering the (change) event handler.

The key, then, is to think of the final consumer first, which is the async pipe in the template. It wants the filtered data, so you start with data$ = . The immediate source of the filtered data, searchTerm => fetchData(searchTerm) needs a search term. Without worrying about where the search term comes from, we’ll assume that there is an observable of it that we can chain off of. So we’ll just write searchTerm$ before we’ve defined it, and chain off of it with a switchMap(). Then we can figure out where those search terms are going to be pumped into that observable by setting up the event handler for (change). So, by thinking backwards, we are able to design a solution with one less thing to keep track of at a time.

Let’s encapsulate this process in a couple of steps:

  1. Identify the consumer
  2. Name an observable to chain off of, not worrying about what will put values into it
  3. Connect the consumer with this new observable
  4. Define the first observable and where it gets its own values from

If we need to handle multiple asynchronous events occurring in series, we can repeat Steps 1–3 for each observable chain, until we reach the very first observable (Step 4).

Now I want to try this process out on a much more complex problem. But this is getting really long, so I’m going to leave that for my next post.

Conclusion

Reactive programming can help us avoid many costly bugs, but becoming comfortable with it is a major hurdle. I believe that the web development community will continue to discover solutions that will make reactive programming much easier, and this will save developers a lot of frustration, time and resources.

Thanks for reading! If you saw my last post, you may remember at the end that I said my next post would be building off of it. I was working on that post when I got side-tracked with this post. So that other post is still coming.

Anyway, please share your thoughts in the comments! What other tools do you know of? What would you like to see? Have you used a process like the one I described here? What other tips do you have?

--

--