State Streams and React

Why RxJS is the Better Tool for Unidirectional Data Flow

It has been a while since we adopted React at work. It was the days before Redux and the emphasis in the community was on unidirectional data flow. The two big names in Flux at the time (other than the bare bones library) were Flummox and Reflux. With a large business app to build that is supposed to live for a long time, the rapid change in the Flux landscape has given me some sleepless nights.

One thing that I didn’t want in our growing code base was an erratic usage of Flux implementations. So instead of jumping ship every couple of months we rather evaluated the new Flux libraries as they rose in popularity. Redux has been the most popular Flux-ish implementation to date, but it does ask a lot from our code base. The migration path is steep, the learning curve is rather steep, and in the mean time we’ve got features to deliver with new financial backers on board. The decision to switch to Redux would be a big one.

Then ReactConf came along and planted a seed. There were two interesting talks about using RxJS with React by Andrew Clark and Gabe Scholz. Not too long thereafter, Ben Lesh from Netflix gave a talk on RxJS version 5, which in turn lead me to Jafar Husain’s talk on using RxJS at Netflix.

These talks forced me to go back to first principles. What is it that I care about w.r.t. state management? Turns out only two things really:

  • Code that will still be relevant 4 to 5 years from now
  • Unidirectional data flow

With these two principles I set out to evaluate using RxJS as the main data flow.

1. Longevity and Relevance

A good indicator of a project’s future longevity, is whether the maintainers are using the project themselves in production. ReactiveX was announced by Microsoft before they started going open source (around 2010) where they created it for themselves to be used in various projects. Matthew Podwysocki (Principal Software Engineer at Microsoft) implemented the Javascript incarnation of ReactiveX and has helped many companies successfully adopt Rx.

RxJS was open sourced in 2012 and has been maintained incredibly well ever since. It’s not everyday in the Javascript world where you get a graph like this:

RxJS version 4 commit history

Netflix is also heavily invested in Rx. They use it on the web, server and on mobile. In a community effort they have put Ben Lesh (Senior Developer at Netflix) on Version 5 of RxJS in order to improve performance, debugability, interoperability between other reactive libraries and to align with the ECMA specification for Observables. All things set aside, this alignment with the specification is a big check in the relevancy box. Development on version 5 has been going on steadily since March 2015 and is looking really good, with notable names such as André Staltz from Futurice also contributing substantially. Big performance gains have been made.

Google is also on board, as the Angular team have fully bought into RxJS and will use it as the main asynchronous primitive. This is big news. If you want to use Angular 2 then you will need to learn RxJS.

2. Unidirectional Data Flow

A stream, as the language suggests, is able to express unidirectional data flow. The real beauty of streams are their high composability and immutability. Code written with RxJS is functional, where each composition or transformation, leaves the original stream untouched. This is a solid foundation to write predictable and flexible code with. Map, merge, filter, reduce, you name it, you can do it with streams.

Event emitters on the other hand, are like firing bullets. Once the bullet has left the barrel, it flies straight until it hits the target. They leave little opportunity to compose or transform the flow.

So to briefly summarise, I think that RxJS comes out on top in both categories. Its core is coming to Javascript, big companies are heavily invested, it’s got a 5 star maintenance history spanning 4 years, its maintainers are using it in production, great improvements are on the way with version 5, and it’s more than just a Javascript library (with RxJava, RxAndroid, RxSwift and more). From an academic stand point, its composable, functional and flexible nature makes it the better tool for unidirectional data flow in my opinion when compared to the event emitter approach of past and present Flux implementations.

Another thing that is worth pointing out is the Prior Art section of the Redux docs (http://redux.js.org/docs/introduction/PriorArt.html), which praises Rx and also states:

Well, it sort of is... The crux of a state stream is two lines of code:

  ...
.startWith(initialState)
.scan(reducer)
...

Basic Usage of State Streams

A state stream is effectively the result of reducing a stream of actions. It boils down to the simple flow:

Action Stream -> Reduce -> State Stream

Lets have a look at a basic usage of this data flow.

Action Streams

Like Redux, we can make use of actions in state streams. For the sake of keeping things tidy, we’ll make use of Flux Standard Actions (FSA):

{
type: SOME_ACTION_TYPE,
payload: { ... },
error: true/false,
}

We can dispatch these actions with a Rx Subject:

Now we have a raw action stream which we can tap into and transform:

* If you are familiar with RxJS then you would have picked up that filterAction, pluckPayload, flatAjax and mapAction aren’t standard RxJS operators. I added them after it became clear that I was repeating myself a lot. They make life a little easier when dealing with action streams

That’s it. The above gives us two action streams that we can use to reduce state with. In this case we’re making a simple AJAX call (with a promise-based service), but more complex data flow can be composed here with RxJS.

And because streams are composable, we can merge these two action streams into one in order to easily reduce state with them:

const action$ = Observable.merge(
search$,
searchResponse$
);

State Streams

All that is left to do is to kick-off with the initial state and reduce as actions flow though:

Job done. If you were wondering, the searchReducer is a plain old reducer function (ala Redux):

function searchReducer(state, action) {
switch (action.type) {
case SEARCH:
return {
...state,
searching: true,
};
...

Basic usage:

Advanced Usage of State Streams

The functionality provided in the basic usage isn’t enough to build complex web applications with. In order for RxJS to truly be an option for unidirectional data flow it will also need to provide:

  • Single source of truth for application state
  • Access to application state when composing actions (thunk middleware in Redux)
  • Side effects such as logging actions and state changes
  • Server rendering with state hydration
  • Ability to compose state from multiple modules when connecting to components
  • Ability to clear state on unmount

The Store as a Collection of State Streams

When working on large React apps, you want to keep away from unnecessary DOM reconciliations. It is one of the biggest performance hits.

In Redux, each connected component subscribes to the whole store, meaning that each and every state change in the app will notify all connected components (by ‘connected component’ I am referring to components that you connect to the Redux store in order to receive state updates).

Each connected component is therefore forced to shallow compare in the shouldComponentUpdate lifecycle method because it doesn’t know if it just received a state update which is relevant or not. This doesn’t seem ideal to me as every state change in your app will result in n-1 (ish) no-ops (where n is the number of connected components).

On the other hand, when the store is modeled as a collection of state streams (one per module), then it allows the connected components to pick and choose which state streams to subscribe to. This means that it will only be called into action when new state arrives in the state streams it’s subscribed to, and thus all those no-ops disappear along with the need to shallow compare in the shouldComponentUpdate lifecycle method (to clarify, further down the tree of the connected component you would still need to shallow compare if you wanted to optimise the reconciliation of the children).

With the above in mind, I started to experiment with modeling the store as a collection of state streams, which I have published as a highly experimental library called Udeo.

https://github.com/mcoetzee/udeo

https://github.com/mcoetzee/react-udeo (its React bindings)

The Thinking Behind the API

As mentioned before, the two core building blocks of a module’s state stream is its action stream and its reducer (of said action stream). The rest is boilerplate that can be abstracted away. Furthermore, in order to handle server rendering, the raw action stream (which I referred to above in the Basic Usage section) will need to originate from the store . The reason why is that each request on the server needs to create a new store, through which dispatched actions need to flow in order to reduce state for the components particular to that request. For more info you can read the Redux docs on server rendering.

The conclusion was then that upon store creation, each module would need to provide two things:

  1. A function (called flow) that receives the raw action stream as argument and returns an array of action streams (derived from the raw action stream)
  2. A function (called reducer) to reduce state from these action streams (this is the plain old reducer function ala Redux)

Here is an example of a module (the details of the reducer have been omitted because we all know how they work):

modules/finder/index.js

Of course you can arrange the code as you see fit.

Creating the store

With the modules in place, we can create the store:

index.js

Store Usage Without Bindings

The two most important methods the store provides are:

store.getState$(moduleName); // Returns the module's state stream
store.dispatch(action); // Dispatches the action

Below we’re subscribing to the finder and shoppingCart’s state streams respectively:

Notice that the shoppingCart subscription above did not get notified when we dispatched the SEARCH action. This is the desired behaviour.

Because streams are composable, we can combine them if required. Lets say our finder module needs to display the total cost of all items added to the shopping cart. We can do this with the combineLatest operator:

State Hydration

Initial state can be loaded into the store upon creation. This is particularly useful for universal apps:

const initialState = window.__INITIAL_STATE__;
const store = createStore(modules, initialState);

Logging Actions and State Changes

RxJS makes it easy to add side effects as a stream flows, which is how we log state changes:

store.setMiddleware(
(moduleName, action, oldState, newState) => {
console.log(...);
}
);

Connecting React Components

With the React bindings we can connect our React components to the store. In the example below we’re subscribing to one module’s state and mapping dispatch to props:

components/Finder.js

But lets say we have that same total cost requirement as before. The Connector can handle it:

For more advanced usages of the Connector, please see the readme: https://github.com/mcoetzee/react-udeo

The store is provided to the connected components as we do in Redux, with a Provider component:

index.js

Accessing State in Action Streams

In addition to the raw action stream, the flow function receives a second argument providing:

{
getState
: Returns the application state
getState$: Returns state streams by module
dispatch: The store's dispatch function
}

Let’s say that the finder module needs to send the current user’s id along with its query to the server. Seeing that we have the application state handy and that the navBar module already has the current user, we can add it into our action stream with the map operator:

Conclusion

Before ReactConf, the notion of choosing a long-term state management strategy seemed near impossible to me with the rapid change in the Flux landscape. But with RxJS’s future longevity and relevance looking really solid, and its ability to produce highly composable data flow, I am a bit more optimistic. I hope to have shown with my experiment that an RxJS-based state stream container provides the tools you need to build complex web applications with and that RxJS should be considered when choosing an unidirectional data flow strategy.