This, in contrast to my previous pieces, will be a more opinion based article. So, dear reader, treat everything here with a grain of salt - it's just my feelings, thoughts and ideas related to the problem of state management in React.
Why would you listen to me?
I worked in commercial projects in React that utilized all of the 3 most popular approaches to state management:
- simply using React built-in state mechanisms,
- using Redux,
- using Mobx.
So in this article I will compare those 3 options.
My goal is to present you a balanced opinion on each of this approaches, but more importantly, give a (surely controversial) opinion on why exactly state management became such important problem in React apps, causing people to write countless libraries, articles and conference talks on the topic, that probably should have been solved a long time ago already.
Let's get started!
When I was first learning front-end development, no one talked about "state management". Nobody really cared about state.
In a first commercial app I worked on, written with the immortal jQuery library, people were simply storing state in some random places (like "data-*" property of some HTML element), or not storing it anywhere at all.
In that second case, reading state meant simply checking what is currently rendered in the DOM. Is that dialog window open? There is no boolean telling us that, so let's just check if there is a DOM element with some special class or id in the tree!
Of course this approach resulted in extremely messy and buggy codebase, so the approach of React, where the state of the application is clearly separated from the view, was a huge epiphany for us and it was the moment when the concept of application state was ingrained in our minds forever.
Since React introduced the concept of state as a separate entity, it also introduced some simple tools to manage that state.
Earlier it was only a
setState method which allowed to modify state stored in a given component. Currently we also have an
useState hook, which has some superficial differences, but ultimately serves the same purpose - defining and modifying state on a per component basis.
Now this last information is the key here. In React each piece of state is defined "inside" the component. So not only a hypothetical component
FirstComponent will have a state independent from the state of
SecondComponent, but even each instance of
FirstComponent will have it's own instance of state. This means that (at least out of the box) there is no sharing of state between React components. Each has its own state instance that it creates an manages and that's it!
But it turns out that we quite often want to display the same state in different places of the website (and hence, in different components).
For example the number of new messages in Facebook header at the top of the application should be always equal to the number of unread messages at the bottom, in the messenger window itself.
Having a shared state - a list of messages, some of which are marked as "unread" - would make that trivial, ensuring that both components always show the same information.
Messenger component would simply display the messages from the list, marking the unread ones with a bold font. At the same time
Header component would count how many messages are marked as unread on the list and would display that number to the user.
As an alternative, having two separate copies of that state - one in
Header component and one in
Messenger component - could result in those states getting out of sync. User might see for example that there are two unread messages in the
Header, but then he would not find any unread messages in
Messenger. That certainly would be annoying.
So how would we achieve state sharing, using only React, without any additional libraries?
A canonical way to share state is to store it in a single component, somewhere higher in the component tree. Then you can simply pass this state down as props. So you can pass the same state to two separate components via props and... boom! Those two components are now sharing that state.
This works very well at the beginning. But if you write your applications this way (and if they get complex enough) you will quickly notice that a lot of your state "bubbles up" as the time goes on.
As more and more components need access to the same state, you put that state higher and higher in component tree, until it finally arrives to the top-most component.
So you end up at some point with one massive "container" component, which stores basically all of your state. It has tens of methods to manipulate this state and it passes it down to tens of components via tens of props.
This quickly become unmanageable. And there is really no clean or easy way to somehow divide this code into smaller pieces. You end up with one massive component file, that often has more than a thousand of lines of code.
You end up with a similar mess as you had before you used React to separate out the state from the view. Yikes...
Redux was invented for a bit different reason than what we described above. In fact, it was conceived purely as a presentation tool, to show the potential of "time travel" in developing React applications.
It turns out that if you put all of your state in one place (called "the store") and you always update it all in one step (using a "reducer" function), then you basically get a capability to "travel in time". Since you can serialize the state you keep in your store and save it after every update, you can keep the history of all the past states.
Then you can simply come back to any of those past states on command, by loading them back to the store again. You are now time traveling - you travel back in time in the history of your application.
Time travel was conceived as a method that would help to develop and debug React applications. It sounds great and people flocked to the idea immediately.
But it turns out that this capability is not as useful as people initially thought. In fact, I believe that most of currently existing Redux applications do not utilize time travel in any significant way, even for debugging purposes. It's simply too much hustle for what is worth (and I am still a big believer in
There is however a quality of Redux that, I believe, made it a staple of programming complex React applications since the very beginning.
As we said, the state in Redux is not created anymore on a per-component basis. Instead, it is stored in a central, in-memory database, called - as we mentioned - the store.
Because of that, potentially any component has access to this state, without passing it down via props, which is simply too cumbersome. In Redux, any component can access the store directly, simply by using a special utility function.
This means that any data that you keep in the store can be displayed, with very little effort, in any place of your application.
Since multiple components can access the state at the same time without any issues, state sharing also stops being a problem.
Our Facebook website can now display the number of unread messages in any place we want, provided we keep the list of messages in the store.
Storing all the state in one place might sound a bit similar to how we kept all the state in a single component. But it turns out that, since updates on Redux store are done by reducer functions, and functions are very easily composable, dividing our Redux codebase to multiple files, split by domain or responsibilites is also much easier than managing one massive "container" component.
So Redux really sounds like a solution to all of the problems that we described before. It might seem that state management in React is solved and we can now move on to more interesting problems.
However, as it is in life, the truth is not that simple.
There are two more pieces of Redux that we did not describe yet.
Although the components can read the Redux store directly, they cannot update the store directly. They have to use "actions" to basically ask the store to update itself.
On top of that, Redux is conceived as a synchronous mechanism, so in order to perform any asynchronous tasks (like HTTP requests for that matter, which is not a crazy requirement for a web app), you need to use a "middleware" which grants your Redux actions asynchronous capabilities.
All of those pieces - the store, reducers, actions, middleware (and a whole bunch of additional boilerplate) make Redux code extremely verbose.
Often changing one simple functionality in Redux results in modifying multiple files. For a newcommer it's extremely difficult to track what is happening in a typical Redux application. Something that seemed simple at the beginning - storing all the state in a single place - quickly turned into extremely complex architecture, that takes literally weeks for people to get used to.
People obviously felt that. After the success of Redux, there was a massive influx of various state management libraries.
Most of those libraries had a thing in common - they tried to do exactly the same thing as Redux, but with less boilerplate.
Mobx became one of the more popular ones.
In contrast with the focus of Redux on functional programming, Mobx decided to unapologetically embrace old-school Object Oriented Programming (OOP) philosophy.
It preserved Redux's concept of the store, but made it simply a class with some properties. It preserved Redux's concept of actions, but made them simply methods.
There were no longer reducers, because you could update object properties like you typically would in a regular class instance. There was no longer a middleware, because methods in Mobx could be both sync and async, making the mechanism more flexible.
Interestingly, philosophy remained the same, but the implementation was vastly different. It resulted in a framework that - at least at the first glance - seemed more lightweight than Redux.
On top of that, Mobx was speaking the language much more familiar to regular software developers. Object Oriented Programming was part of a typical programmers education for decades, so managing state in terms of classes, objects, methods and properties was much more familiar to the vast majority of programmers getting into React.
And once again it might seem that we have solved our problem - we now have a state management library that preserves the ideas and benefits of Redux, while being less verbose and less alien to newcommers.
So where is the problem? It turns out that while Redux is openly complex and verbose, Mobx hides it's complexities, pretending to be a programming model that is familiar to majority of developers.
It turns out that Mobx has more in common with Rx.js or even Excel than traditional OOP. Mobx looks like Object Oriented Programming, while in fact it's core mechanism is based on vastly different philosophy, even more alien to regular programers than functional programming, promoted by Redux.
Mobx is not an OOP library. It's a reactive programming library, sneakily hidden under the syntax of classes, objects and methods.
The thing is, when you are working with Mobx objects and modifying their properties, Mobx has to somehow notify React that a change to the state has occured. In order to achieve that, Mobx has a mechanism that is inspired by reactive programming concepts. When a change to the property happens, Mobx "notifies" all the components that are using that property and in reaction those components can now rerender.
This is simple so far and it works flawlessly, being one of the reasons why Mobx can achieve so much of Redux's functionality with so little boilerplate.
But reactiveness of Mobx doesn't end there.
Some state values depend on others. For example a number of unread messages depends directly on the list of messages. When a new message appears on the list, the number of unread messages should in reaction increase.
So in Mobx, when property changes, the library mechanism notifies not only the React components displaying that property, but also other properties that are depending on that property.
It works just like Excel, where after you change the value of one cell, the cells that depend on that value are in reaction immediately updated as well.
Furthermore, some of that properties are calculated in an asynchronous manner. For example if your property is an article id, you might want to download from the backend the title and author of that article. These are two new properties - title and author - that directly depend on a previous property - article id. But they cannot be calculated in a synchronous way. We need to make an asynchronous HTTP request, wait for the response, deal with any errors that might happen and just then we can update the title and author properties.
When you start to dig dipper, you discover that Mobx has plenty of mechanisms and utilities for dealing with those cases and it is a style of programming that is explicitly encouraged by Mobx documentation. You start to realize that Mobx is only Object Oriented on the surface and is in fact governed by a completely different philosophy entirely.
What is more, it turns out that this graph of properties and their dependencies quickly becomes surprisingly complicated in a sufficiently big application.
If you have ever seen a massive Excel file that is so big and complicated that everyone is too scared to make any changes to it - you have basically seen a Mobx app.
But on top of that, Mobx reactiveness mechanism is not directly accessible or visible to the developer. As we said, it is hidden under OOP syntax of classes, methods and decorators.
Because of that a lot of what Mobx does is simply "magic" from a programmers perspective. I have spent many hours scratching my head, trying to figure out why, in a certain situation, Mobx's mechanism does (or doesn't do) some updates. I had moments where my code was mysteriously sending multiple HTTP request instead of one. I also had moments where my code wasn't sending any request, even though I could swear it should.
Of course in the end the errors were always on my side. Mobx works exactly as it should.
But while Redux is complex because it basically gives all the pieces into your hands and asks you to manage them, Mobx does the exact opposite, by hiding it's intricacies from you and pretending it's just a "regular" OOP library.
One approach causes the code that is full of boilerplate, multiple files and difficult to track relations between different parts of the codebase.
The second approach causes the code that looks slim and elegant, but then from time to times it does things that you don't expect and are difficult to analyze, because you literally don't understand what the library does underneath.
Interestingly, this whole article was written under the premise that shared state is a common requirement of many modern web applications.
But... is it really?
I mean, of course, you will sometimes have to display a number of unread messages in two completely different places in your application.
But is that really enough of a reason to create a complex state management solutions?
Maybe... maybe what we need is literally just a way to share state between components in a manageable manner?
I am imagining having a
useSharedState hook, which would work just like a regular React state hook, but would allow components to access the same state instance, for example by sharing a predefined key:
const [count, setCount] = useSharedState(0, "UNREAD_MESSAGES_COUNT");
In fact this idea not new at all. I have seen at least a few implementations of a hook similar to this one.
It seems that people are (consciously or not) feeling the need for this kind of solution.
Of course it doesn't solve all of the problems yet. The biggest one being that asynchronous code (in particular data fetching) is still incredibly awkward in modern React and implementing it in modern hook syntax feels almost like a hack (in fact, I will probably write a follow up article on that exact problem).
But I will still hold my controversial claim which I promised you at the beginning of the article:
All this mess with state management debates, thousands of libraries created and articles written, stems mostly from a single reason - there is no easy way in React to share state instances between components.
Now bear in mind - I never had an occasion to write a full, commercial application using this hypothetical
useSharedState hook. As I mentioned, there would be still some things needed to make such an application really easy to develop and maintain.
So everything I say now might be completely misguided, but I will say it anyway:
We over-engineered state management in React.
Working with state in React is already close to being a great experience - separating state from the view was a huge stepping stone - we only lack a few little solutions to very specific problems, like sharing state or fetching data.
We don't need state management frameworks and libraries. We just need few adjustments to the core React mechanism (or simply a few tiny utilities in an external library).
Writing our massive web applications will always be complicated. State management is hard. In fact, the bigger your app is, the exponentially harder it becomes.
But I believe that all this time and effort that goes into learning, debugging and taming state management libraries could be instead devoted to refactoring your application, architecting it more carefully and organizing the code better.
This would result in a code that is simpler, easier to understand and easier to manage by your whole team.
And I see that this is a turn that React community is already slowly doing, being more and more vocal about being disappointing by programming with Redux or Mobx.
Of course Redux and Mobx still have their place. They are trully great libraries. They solve very concrete problems and bring specific advantages to the table (and specific drawbacks at the same time).
If you want to dabble into time traveling debugging or you need to store your serializable state in one place (for example to save it on the backend or in local storage), then Redux is for you.
If your applications state is highly interconnected and you want to make sure that updates of one property will result in immediate updates of other properties, than Mobx model will fit that problem very well.
And if you don't have any specific requirements, just start with vanilla React.
I described some issues with "vanilla React" approach in that article, but it is a completely different thing to encounter those problems by yourself in practice. Having this experience, you will be better informed to make a smart decision on which state management solution to choose.
Or not choose. ;)
Thanks for reading!