Experimenting around React's scalability
Writing backend code has always been my preference because it allows more structured code, since backend often means Java in my industry, the web. On the other hand frontend coding has always been a pain because of the mess. Javascript may be the culprit, but probably more because it allows to be messy more than being messy itself. Then one day I tested Angular (the v1) and I had the wow effect: first time I felt like an engineer writing Javascript code. Then appeared React: awesome, about same idea but just a library not a full framework. Our team at Scoop.it adopted it and embraced it.
We embraced it so much that we felt the pain in writing big React pages. Raw React doesn’t scale well. People already faced this scalability issue. They came with the idea of Flux (thank you people at Facebook to have shared it with us).
While reading about Flux I started designing a way of managing the state. Later some smart guys of our team dived and fell in love of Redux, an open source implementation of Flux. I learned from them and I made improvement to my design. Here is the story.
React ? Flux ?
If I were to define React in one sentence, I would write: React let you define a component which build a virtual DOM from a state, and React will transform the virtual DOM into optimized real DOM updates each time the state of the component is updated. React has a great documentation if you want to learn more about it. The topic which we will study here is the state, which is internal to the component, and thus difficult to manage from outside. In raw React, pushing data into it is just about setting some property (the React props) and getting data out is done via callbacks.
As in many technologies, too many callbacks create a mess. Then comes to the rescue the design pattern Flux. The pattern is about a cycle, and in the implementation Redux it can be described like this: a global state which parts are being used in React components; a React component can dispatch actions; every action is funneled into the reducers; a reducer handle some specific actions to build the next global state. So this is the core idea of Flux: one big cycle rather than a web of React props and callbacks.
A first hazardous design
While I was reading about Flux, nothing was ready to be used at that time. By ready I not only mean the code, but also the tutorials, the blog posts, the stackoverflow answers. Not being an experienced Javascript dev I prefer write some little piece of simple code I own rather than use powerful framework I don’t understand fully. So I started to write my own messy code and see where it leads me.
I have to admit I was not totally convinced by the central flow idea. Ideas such as central, unique, global always spook me. Such designs are often non reusable, not composable. And I love composition. I don’t compromise easily with composition.
Still, inspired by the pattern, in some part of our React code I have started to split the code between the one which build the DOM and the one which manage the state. I did it in the same way I design backend code, splitting concerns: on one end the view and the controller and on the other hand the model. And between the two there is a flow of data, via function calls and a stupid implementation of the listener pattern.
So here is a piece of what I have came about:
var SuggestionStore = { _listeners = []; _notify: function() { for(var i = 0; i < this._listeners; i++) { this._listeners[i](); } }; register: function(listener) { _listeners.push(listener); }; unregister: function(listener) { _listeners.remove(listener); }; _sugs: null, _fetchSuggestions: function() { jQuery.ajax("/getsuggestions", function(data) { this._sugs = data; this._notify(); }.bind(this)); }, getSuggestions: function() { if (this._sugs === null) { this._fetchSuggestions(); return null; } return this._sugs; }, deleteSuggestion: function(sugId) { jQuery.ajax("/deletesug", {"id": sugId}, function(data) { this._sugs = data; this._notify(); }.bind(this)); }, }; var SuggestionList = React.createClass({ componentWillMount: function() { SuggestionStore.register(this._onSugEvent); }, componentWillUnmount: function() { SuggestionStore.unregister(this._onSugEvent); }, _onSugEvent: function() { this.setState({}); }, render: function() { var sugs = SuggestionStore.getSuggestions(); if (sugs === null) { return (<div>Loading...</div>); } return ( <div> { sugs.map(function(sug) { return ( <Suggestion key={sug.id} sug={sug} /> ); }) } </div> ); } }) var Suggestion = React.createClass({ onClickDelete: function(e) { SuggestionStore.deleteSuggestion(this.props.sug.id); } render: function() { return ( <div> <h2>{this.props.sug.title}</h2> <button onClick={onClickDelete}>Delete</button> </div> ); } }
Review
So there is a singleton, SuggestionStore. Not very composable. As I have warned, this is my messy experimental code (which runs in prod!).
Any people knowing about Redux will have its eyes bleeding, for sure, but this code is an actual improvement of the code where every data is propagated via props and callbacks. Note how is handled the deletion of a suggestion.
The code was relatively easy to debug, the code is nicely trivial. It is maintainable. Not that messy. Job accomplished.
I even coded another store which was handling some configuration of the suggestion engine. And I like very much that two distinct parts of the business logic ended up into two distinct parts of the code. That is some nice separation of concerns compared to the spaghetti code with callbacks and React props.
Limitations
The code is far from smart and optimized though. It has serious limitations. The setState({}) is blindly requesting a render even if the part of the store being used didn’t changed. In the end, one update in a store triggers a full render. Thanks to React the full render is just about running javascript, only the changing part in the real DOM is updated. But still, it is useless cpu cycles.
That experiment has shown me that we can better scale thanks to my crappy Store pattern, which is a good start. The real code in Scoop.it is longer, there is more stuff, but it is not that complex, definitively not as complex as what Facebook had to deal with when designing the Flux pattern. So let’s see real smart design.
Enters Redux
I had no doubt that an army of great brains will come with something that is better than my sketches. They built Redux, a popular (de facto standard ?) implementation of Flux. Considering the joy my colleagues have coding with it, it is a nice piece. So I let myself brought up to speed to how they use Redux. It is a mix of react-redux obviously, but with redux-actions and redux-saga. I cannot comment on these particular choices of flavor but from where I stand it seems a smart choice.
Saga magic
On one hand the stores I designed are beyond basic, on the other hand the so called sagas are magic; if not black magic, javascript magic. Sagas are placed at the interception of the actions to do complex business logic, more than just set data into the global state, it may involve some asynchrounous HTTP request for instance. Since there are a lot of asynchronous code, to avoid a mess of callbacks, sagas leverage the Javascript generators to make asynchronous code look like sequential, thus easier to read.
Here is an example from the tutorial:
export function* incrementAsync() { yield delay(1000) yield put({ type: 'INCREMENT' }) }
While it is quite readable, I don’t find it trivial to understand. There is a lot of hidden stuff. The put function here is setting some data into Redux’s global state. But the global state doesn’t appear to be referenced anywhere here! This looks like code with side effects, even if it doesn’t. So weird.
And apparently this is the simplest way of coding complex action handlers. I have to admit that if the business logic has to pipe promises, in plain javascript it will be less easy to read than the nicely apparently sequential code of a so called saga. In all the business logic I had to write, there is one case where two promises must be chained. It is probably a simple enough use case that the new await and async javascript features could handle nicely. So I bet the simplification brought by saga is more due to Redux API (cf the put function) than to business logic.
Reusable React components
The React components are nicely separated from the plumbing of Redux. A React component only rely on some props to get the data it needs, and some other props acting as callbacks to dispatch actions. The actual definition of how to get the data into the props and providing the actual implementation of the callbacks is separated from the React component. It makes the React components pure, reusable, which is nice. It is the job of the function connect to bind the React component to the global state and the dispatch of the actions. The tutorial nicely shows it there.
But in practice the components are not reusable. Writing a connected component is to indeed write a reusable React component, and to build another component by connecting the pure one to the global state. There is then 2 components, the pure reusable one and the connected one. But when writing a third component which will use what we just created, it is the connected version which needs to be actually used. It makes then the third component implicitely connected even if it is not explicitely connected. It makes the separation of the pure third component and its connected version just code style, not actual usefulness. For instance in the tutorial if the Todo has to be a connected component because its color depends on a specific part of the global state, then TodoList cannot be reused in any other context, since one of its children is already connected a specific way to the global state.
So when building a reusable component, thus not connected, the sub components which are part of it, are necessarily not connected either.
So the reusability is actually limited. And the effort to split the pure component from the connected one is useless. Either build a unconnected reusable component, just like any other reusable React component, or build a connected one which knows about the global state and the actions. On that topic it is not better than I have coded with my Stores.
Copy/Paste
A part I find dumb to do in Redux is the definition of actions. Seen from a high level they are basically identifiers which, instanciated, bring a payload. Due to the unique flow of Flux/Redux, you must ensure that the identifiers don’t conflict. Since it is implemented with strings, anything bad can happen easily. And it is a little bit frustrating because actions sound like functions. And functions, as in any language, have a unique identifier, their names, that even tools (compilers!) can check for duplicates automatically for you.
And creating an action is not just an actual identifier, there are some boilerplate stuff which makes them dumb copy/paste.
Same for the reducers which just set data into the state. Thus a lot of boilerplate code to write.
And apparently without the library we are using, it would be even worse.
Store.js better than Redux ?
So what am I missing with my Stores that Redux have ?
Well, let’s be honest, I am just playing with Javascript code here. And the design of my Stores is not answering the scalability issues Facebook had when designing Flux. The main feature behind Flux is that big workflow which is avoiding the chain of updates mess. There is one big nice cycle which is simple to reason about. With many Stores, it is harder to reason about which should be aware of the update of the other. Because we should also avoid update loops.
And that is the theory, because the use case I work on doesn’t need that much scaling. Actually the use case need more reusability (I had to design four different suggestion engines, sharing parts). And I know nothing better than composition to answer it. Redux is not answering it better than my sketches.
There are also all the things around which are very important: the community with their blogs posts, thread of tweets, the answers on stackoverflow, the colleagues which know a large bunch of the extra node modules which helps the life of a Reduxer, and last but not least the debug tools in the browser.
Once again, I would question the real need for that. It seems to me that all that extra help is needed because of the generated complexity by Redux. The code I wrote is so simple, what would you need to debug more than the Javascript debugger ?
But hey, let’s stop messing around with false arguments, and let’s point what is really missing.
Singleton
As already pointed out, the real horror for me is that a Store is a singleton. In Redux the state is instanciated when we start to need it, at the root of the components you want to connect. And you can have several roots. It is then propagated via the React’s context, available in any connected component.
This is where I see the React’s context as the equivalent of Spring’s application context in the Java world. In Spring vocabulary, beans are instanciated and injected transparently in the other beans which requires them. Here I would then instanciate Stores, let React’s context transport them transparently, and the components would get the store which they required.
So this issue is resolvable with few lines of code. More code are required here than with Redux, but at least there is a simple solution.
Performance
Also as pointed out, the update of the store is so dumb that everything rerenders on any update. This makes the design of the store not scallable at all.
What is needed, put it simply, is to select the data required by the component and rerender the component only if that selected data change. And it is a way harder to implement than to define (I already give it a try).
Among the things to handle, there is the first time the component needs data. React API is not easy to work with about its lifecycle. Even if it has an awesome documentation, there are some things that are described as may happen, and that is quite disturbing.
Then there is the detection of the update of data. What is an update ? Even with the opinionated definition that the data is an object and that there should be a comparison by keys of the values of the object, this is not trivial.
There are also data which are computed from other data from the state. Such computation may require to be put in a cache to avoid recomputing it. And as every developer know, anything involving a cache involve unexplainable bugs.
Last but not least the integration of React should be smart enough so that the implementation of componentShouldUpdate on the React component needs to be correlated to the update of the selected data and to the calls to the function render (once rendered, no need to rerender). And what make it very tedious to implement is that is depends on the props of the React component, how they update, how they impact the selected data. Definitively not trivial to implement. There is not much code in Redux, but there is a lot of comments explaining why exactly the few lines are there, showing this is a delicate subject (and that Redux developers are great craftsmen).
Then let’s use Redux ?
Since I am not an experienced Javascript developer, always eager to learn by practice, I did the forbidden thing: reinvent. Reinventing the wheel is obviously lost time for the project, but not for the coder. It is a great way to learn, train, be better at knowing the language. So I continued not using Redux and make improvements to the Store (and I have to admit that this was a nice excuse to avoided Redux since it is not much fun due to the boilerplate code to write). I started by the simplest thing: use the context to avoid singletons. I then started to write Javascript classes (long live ES6!) to handle the pipelining of data update from the store into the component.
Generifying the code, I came up with an abstraction, which I called ReactDAO. Yes, I know. WTF with DAO. On purpose to illustrate that my code is part of some experiment and not a replacement for any well designed and well thought technology out there. But still, it seems a good name since it does more that storing data, it also offers different way to access them, abstracting how there are actually stored, and some of its functions may require to fetch data from out there.
So I did the simplest improvements, not including the harder ones as implementing componentShouldUpdate, so the quality and performance is not at what Redux can do. On the other hand it is still globally simple, non-magical and not too verbose; migrating to Redux will mostly be… (opinionated intuition incoming…) a PITA.
One thing is getting a little complex though: the selection of the data and properly computing the associated updates. It seems a complex subject, since it doesn’t seem simpler with Redux. We had an issue with a so called selector in the part of our app which uses Redux, it involved a lot of us and a lot of passion in the discussion we had about how to fix it.
Still not yet there
Since I am not a genius (oh!), the topic about the selection of data and triggering only the needed renders was not that well answered by my code. It is working nicely, without weird bugs of some sort, but the performence is still behind what Redux is capable of. I don’t regret writing my sketches, since I learned a lot. I would redo it, the final quality of what I have produced is good enough to be put in prod. But there is better and this is Redux. So the next step, which shouldn’t be that hard since some concepts are shared, would be to migrate to Redux. "Would be". Maybe.
Maybe because one day I got pointed out this article where they explain the pain points they identified with Redux, which I share. And they explained their solution to deal with state management. I am not convinced it is better, since they decided to use inheritance. And in that battle between inheritance and composition, I’ll always prefer the later. The way they define a controller in a MVC pattern is suspicious too. But that is not the interesting part.
The interesting part is that while building something away from Redux is that at the end of the article they mentioned that they are using mobx under the hood. I didn’t knew it at all. After reading the tutorial, watched a nice pres about it, and an awesome comparison with Redux, I had the wow effect. All the complicated stuff of the selection of data and the implementation of componentShouldUpdate are provided out of the box. Using this library is damn simple. Way simpler than what is needed for Redux or the ReactDAO. It is so simple that it seems like magic. But I don’t like magic! It might hide some side effect I am not aware of! Especially on the touchy subject which is about knowing how the update of the data is detected and then propagated. So I tried to understand it, I had a try into the code of mobx, but I don’t understand a thing, not a good sign. I have read the documentation with more attention, I sense that they implemented it in a very smart way. I won’t babble about it since I don’t actually know what is going on in mobx, but I have a good feeling about it. React with mobx might be the real deal. I’ll certainly dig into it.